[0-9]+(?:\.[0-9]+)*) # release segment
(?P # pre-release
[-_\.]?
(?P(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P[0-9]+)?
)?
(?P # post release
(?:-(?P[0-9]+))
|
(?:
[-_\.]?
(?Ppost|rev|r)
[-_\.]?
(?P[0-9]+)?
)
)?
(?P # dev release
[-_\.]?
(?Pdev)
[-_\.]?
(?P[0-9]+)?
)?
)
(?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
"""
pattern = re.compile(
r"^\s*" + VERSION_PATTERN + r"\s*$",
re.VERBOSE | re.IGNORECASE,
)
try:
release = pattern.match(version).groupdict()["release"] # type: ignore
release_tuple: "Tuple[int, ...]" = tuple(map(int, release.split(".")[:3]))
except (TypeError, ValueError, AttributeError):
return None
return release_tuple
def _is_contextvars_broken() -> bool:
"""
Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
"""
try:
import gevent
from gevent.monkey import is_object_patched
# Get the MAJOR and MINOR version numbers of Gevent
version_tuple = tuple(
[int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
)
if is_object_patched("threading", "local"):
# Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
# context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
# Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
# Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
# for contextvars, is able to patch both thread locals and contextvars, in
# that case, check if contextvars are effectively patched.
if (
# Gevent 20.9.0+
(sys.version_info >= (3, 7) and version_tuple >= (20, 9))
# Gevent 20.5.0+ or Python < 3.7
or (is_object_patched("contextvars", "ContextVar"))
):
return False
return True
except ImportError:
pass
try:
import greenlet
from eventlet.patcher import is_monkey_patched # type: ignore
greenlet_version = parse_version(greenlet.__version__)
if greenlet_version is None:
logger.error(
"Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
)
return False
if is_monkey_patched("thread") and greenlet_version < (0, 5):
return True
except ImportError:
pass
return False
def _make_threadlocal_contextvars(local: type) -> type:
class ContextVar:
# Super-limited impl of ContextVar
def __init__(self, name: str, default: "Any" = None) -> None:
self._name = name
self._default = default
self._local = local()
self._original_local = local()
def get(self, default: "Any" = None) -> "Any":
return getattr(self._local, "value", default or self._default)
def set(self, value: "Any") -> "Any":
token = str(random.getrandbits(64))
original_value = self.get()
setattr(self._original_local, token, original_value)
self._local.value = value
return token
def reset(self, token: "Any") -> None:
self._local.value = getattr(self._original_local, token)
# delete the original value (this way it works in Python 3.6+)
del self._original_local.__dict__[token]
return ContextVar
def _get_contextvars() -> "Tuple[bool, type]":
"""
Figure out the "right" contextvars installation to use. Returns a
`contextvars.ContextVar`-like class with a limited API.
See https://docs.sentry.io/platforms/python/contextvars/ for more information.
"""
if not _is_contextvars_broken():
# aiocontextvars is a PyPI package that ensures that the contextvars
# backport (also a PyPI package) works with asyncio under Python 3.6
#
# Import it if available.
if sys.version_info < (3, 7):
# `aiocontextvars` is absolutely required for functional
# contextvars on Python 3.6.
try:
from aiocontextvars import ContextVar
return True, ContextVar
except ImportError:
pass
else:
# On Python 3.7 contextvars are functional.
try:
from contextvars import ContextVar
return True, ContextVar
except ImportError:
pass
# Fall back to basic thread-local usage.
from threading import local
return False, _make_threadlocal_contextvars(local)
HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
CONTEXTVARS_ERROR_MESSAGE = """
With asyncio/ASGI applications, the Sentry SDK requires a functional
installation of `contextvars` to avoid leaking scope/context data across
requests.
Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
"""
_is_sentry_internal_task = ContextVar("is_sentry_internal_task", default=False)
# These exceptions won't set the span status to error if they occur. Use
# register_control_flow_exception to add to this list
_control_flow_exception_classes: "set[type]" = set()
def is_internal_task() -> bool:
return _is_sentry_internal_task.get()
@contextmanager
def mark_sentry_task_internal() -> "Generator[None, None, None]":
"""Context manager to mark a task as Sentry internal."""
token = _is_sentry_internal_task.set(True)
try:
yield
finally:
_is_sentry_internal_task.reset(token)
def qualname_from_function(func: "Callable[..., Any]") -> "Optional[str]":
"""Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
func_qualname: "Optional[str]" = None
prefix, suffix = "", ""
if isinstance(func, partial) and hasattr(func.func, "__name__"):
prefix, suffix = "partial()"
func = func.func
else:
# The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
# https://github.com/python/cpython/pull/16600
partial_method = getattr(func, "_partialmethod", None) or getattr(
func, "__partialmethod__", None
)
if isinstance(partial_method, partialmethod):
prefix, suffix = "partialmethod()"
func = partial_method.func
if hasattr(func, "__qualname__"):
func_qualname = func.__qualname__
elif hasattr(func, "__name__"):
func_qualname = func.__name__
if func_qualname is not None:
if hasattr(func, "__module__") and isinstance(func.__module__, str):
func_qualname = func.__module__ + "." + func_qualname
func_qualname = prefix + func_qualname + suffix
return func_qualname
def transaction_from_function(func: "Callable[..., Any]") -> "Optional[str]":
return qualname_from_function(func)
disable_capture_event = ContextVar("disable_capture_event")
class ServerlessTimeoutWarning(Exception): # noqa: N818
"""Raised when a serverless method is about to reach its timeout."""
pass
class TimeoutThread(threading.Thread):
"""Creates a Thread which runs (sleeps) for a time duration equal to
waiting_time and raises a custom ServerlessTimeout exception.
"""
def __init__(
self,
waiting_time: float,
configured_timeout: int,
isolation_scope: "Optional[sentry_sdk.Scope]" = None,
current_scope: "Optional[sentry_sdk.Scope]" = None,
) -> None:
threading.Thread.__init__(self)
self.waiting_time = waiting_time
self.configured_timeout = configured_timeout
self.isolation_scope = isolation_scope
self.current_scope = current_scope
self._stop_event = threading.Event()
def stop(self) -> None:
self._stop_event.set()
def _capture_exception(self) -> "ExcInfo":
exc_info = sys.exc_info()
client = sentry_sdk.get_client()
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "threading", "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)
return exc_info
def run(self) -> None:
self._stop_event.wait(self.waiting_time)
if self._stop_event.is_set():
return
integer_configured_timeout = int(self.configured_timeout)
# Setting up the exact integer value of configured time(in seconds)
if integer_configured_timeout < self.configured_timeout:
integer_configured_timeout = integer_configured_timeout + 1
# Raising Exception after timeout duration is reached
if self.isolation_scope is not None and self.current_scope is not None:
with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
with sentry_sdk.scope.use_scope(self.current_scope):
try:
raise ServerlessTimeoutWarning(
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
integer_configured_timeout
)
)
except Exception:
reraise(*self._capture_exception())
raise ServerlessTimeoutWarning(
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
integer_configured_timeout
)
)
def to_base64(original: str) -> "Optional[str]":
"""
Convert a string to base64, via UTF-8. Returns None on invalid input.
"""
base64_string = None
try:
utf8_bytes = original.encode("UTF-8")
base64_bytes = base64.b64encode(utf8_bytes)
base64_string = base64_bytes.decode("UTF-8")
except Exception as err:
logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
return base64_string
def from_base64(base64_string: str) -> "Optional[str]":
"""
Convert a string from base64, via UTF-8. Returns None on invalid input.
"""
utf8_string = None
try:
only_valid_chars = BASE64_ALPHABET.match(base64_string)
assert only_valid_chars
base64_bytes = base64_string.encode("UTF-8")
utf8_bytes = base64.b64decode(base64_bytes)
utf8_string = utf8_bytes.decode("UTF-8")
except Exception as err:
logger.warning(
"Unable to decode {b64} from base64:".format(b64=base64_string), err
)
return utf8_string
Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
def sanitize_url(
url: str,
remove_authority: bool = True,
remove_query_values: bool = True,
split: bool = False,
) -> "Union[str, Components]":
"""
Removes the authority and query parameter values from a given URL.
"""
parsed_url = urlsplit(url)
query_params = parse_qs(parsed_url.query, keep_blank_values=True)
# strip username:password (netloc can be usr:pwd@example.com)
if remove_authority:
netloc_parts = parsed_url.netloc.split("@")
if len(netloc_parts) > 1:
netloc = "%s:%s@%s" % (
SENSITIVE_DATA_SUBSTITUTE,
SENSITIVE_DATA_SUBSTITUTE,
netloc_parts[-1],
)
else:
netloc = parsed_url.netloc
else:
netloc = parsed_url.netloc
# strip values from query string
if remove_query_values:
query_string = unquote(
urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
)
else:
query_string = parsed_url.query
components = Components(
scheme=parsed_url.scheme,
netloc=netloc,
query=query_string,
path=parsed_url.path,
fragment=parsed_url.fragment,
)
if split:
return components
else:
return urlunsplit(components)
ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
def parse_url(url: str, sanitize: bool = True) -> "ParsedUrl":
"""
Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
parameters will be sanitized to remove sensitive data. The autority (username and password)
in the URL will always be removed.
"""
parsed_url = sanitize_url(
url, remove_authority=True, remove_query_values=sanitize, split=True
)
base_url = urlunsplit(
Components(
scheme=parsed_url.scheme, # type: ignore
netloc=parsed_url.netloc, # type: ignore
query="",
path=parsed_url.path, # type: ignore
fragment="",
)
)
return ParsedUrl(
url=base_url,
query=parsed_url.query, # type: ignore
fragment=parsed_url.fragment, # type: ignore
)
def is_valid_sample_rate(rate: "Any", source: str) -> bool:
"""
Checks the given sample rate to make sure it is valid type and value (a
boolean or a number between 0 and 1, inclusive).
"""
# both booleans and NaN are instances of Real, so a) checking for Real
# checks for the possibility of a boolean also, and b) we have to check
# separately for NaN and Decimal does not derive from Real so need to check that too
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
logger.warning(
"{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
source=source, rate=rate, type=type(rate)
)
)
return False
# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
rate = float(rate)
if rate < 0 or rate > 1:
logger.warning(
"{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
source=source, rate=rate
)
)
return False
return True
def match_regex_list(
item: str,
regex_list: "Optional[List[str]]" = None,
substring_matching: bool = False,
) -> bool:
if regex_list is None:
return False
for item_matcher in regex_list:
if not substring_matching and item_matcher[-1] != "$":
item_matcher += "$"
matched = re.search(item_matcher, item)
if matched:
return True
return False
def is_sentry_url(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
"""
Determines whether the given URL matches the Sentry DSN.
"""
return (
client is not None
and client.transport is not None
and client.transport.parsed_dsn is not None
and client.transport.parsed_dsn.netloc in url
)
def _generate_installed_modules() -> "Iterator[Tuple[str, str]]":
try:
from importlib import metadata
yielded = set()
for dist in metadata.distributions():
name = dist.metadata.get("Name", None) # type: ignore[attr-defined]
# `metadata` values may be `None`, see:
# https://github.com/python/cpython/issues/91216
# and
# https://github.com/python/importlib_metadata/issues/371
if name is not None:
normalized_name = _normalize_module_name(name)
if dist.version is not None and normalized_name not in yielded:
yield normalized_name, dist.version
yielded.add(normalized_name)
except ImportError:
# < py3.8
try:
import pkg_resources
except ImportError:
return
for info in pkg_resources.working_set:
yield _normalize_module_name(info.key), info.version
def _normalize_module_name(name: str) -> str:
return name.lower()
def _replace_hyphens_dots_and_underscores_with_dashes(name: str) -> str:
# https://peps.python.org/pep-0503/#normalized-names
return re.sub(r"[-_.]+", "-", name)
def _get_installed_modules() -> "Dict[str, str]":
global _installed_modules
if _installed_modules is None:
_installed_modules = dict(_generate_installed_modules())
return _installed_modules
def package_version(package: str) -> "Optional[Tuple[int, ...]]":
normalized_package = _normalize_module_name(
_replace_hyphens_dots_and_underscores_with_dashes(package)
)
installed_packages = {
_replace_hyphens_dots_and_underscores_with_dashes(module): v
for module, v in _get_installed_modules().items()
}
version = installed_packages.get(normalized_package)
if version is None:
return None
return parse_version(version)
def reraise(
tp: "Optional[Type[BaseException]]",
value: "Optional[BaseException]",
tb: "Optional[Any]" = None,
) -> "NoReturn":
assert value is not None
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
def _no_op(*_a: "Any", **_k: "Any") -> None:
"""No-op function for ensure_integration_enabled."""
pass
if TYPE_CHECKING:
@overload
def ensure_integration_enabled(
integration: "type[sentry_sdk.integrations.Integration]",
original_function: "Callable[P, R]",
) -> "Callable[[Callable[P, R]], Callable[P, R]]": ...
@overload
def ensure_integration_enabled(
integration: "type[sentry_sdk.integrations.Integration]",
) -> "Callable[[Callable[P, None]], Callable[P, None]]": ...
def ensure_integration_enabled(
integration: "type[sentry_sdk.integrations.Integration]",
original_function: "Union[Callable[P, R], Callable[P, None]]" = _no_op,
) -> "Callable[[Callable[P, R]], Callable[P, R]]":
"""
Ensures a given integration is enabled prior to calling a Sentry-patched function.
The function takes as its parameters the integration that must be enabled and the original
function that the SDK is patching. The function returns a function that takes the
decorated (Sentry-patched) function as its parameter, and returns a function that, when
called, checks whether the given integration is enabled. If the integration is enabled, the
function calls the decorated, Sentry-patched function. If the integration is not enabled,
the original function is called.
The function also takes care of preserving the original function's signature and docstring.
Example usage:
```python
@ensure_integration_enabled(MyIntegration, my_function)
def patch_my_function():
with sentry_sdk.start_transaction(...):
return my_function()
```
"""
if TYPE_CHECKING:
# Type hint to ensure the default function has the right typing. The overloads
# ensure the default _no_op function is only used when R is None.
original_function = cast(Callable[P, R], original_function)
def patcher(sentry_patched_function: "Callable[P, R]") -> "Callable[P, R]":
def runner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
if sentry_sdk.get_client().get_integration(integration) is None:
return original_function(*args, **kwargs)
return sentry_patched_function(*args, **kwargs)
if original_function is _no_op:
return wraps(sentry_patched_function)(runner)
return wraps(original_function)(runner)
return patcher
if PY37:
def nanosecond_time() -> int:
return time.perf_counter_ns()
else:
def nanosecond_time() -> int:
return int(time.perf_counter() * 1e9)
def now() -> float:
return time.perf_counter()
try:
from gevent import get_hub as get_gevent_hub
from gevent.monkey import is_module_patched
except ImportError:
# it's not great that the signatures are different, get_hub can't return None
# consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
def get_gevent_hub() -> "Optional[Hub]": # type: ignore[misc]
return None
def is_module_patched(mod_name: str) -> bool:
# unable to import from gevent means no modules have been patched
return False
def is_gevent() -> bool:
return is_module_patched("threading") or is_module_patched("_thread")
def get_current_thread_meta(
thread: "Optional[threading.Thread]" = None,
) -> "Tuple[Optional[int], Optional[str]]":
"""
Try to get the id of the current thread, with various fall backs.
"""
# if a thread is specified, that takes priority
if thread is not None:
try:
thread_id = thread.ident
thread_name = thread.name
if thread_id is not None:
return thread_id, thread_name
except AttributeError:
pass
# if the app is using gevent, we should look at the gevent hub first
# as the id there differs from what the threading module reports
if is_gevent():
gevent_hub = get_gevent_hub()
if gevent_hub is not None:
try:
# this is undocumented, so wrap it in try except to be safe
return gevent_hub.thread_ident, None
except AttributeError:
pass
# use the current thread's id if possible
try:
thread = threading.current_thread()
thread_id = thread.ident
thread_name = thread.name
if thread_id is not None:
return thread_id, thread_name
except AttributeError:
pass
# if we can't get the current thread id, fall back to the main thread id
try:
thread = threading.main_thread()
thread_id = thread.ident
thread_name = thread.name
if thread_id is not None:
return thread_id, thread_name
except AttributeError:
pass
# we've tried everything, time to give up
return None, None
def _register_control_flow_exception(
exc_type: "Union[type, list[type], tuple[type], set[type]]",
) -> None:
if isinstance(exc_type, (list, tuple, set)):
_control_flow_exception_classes.update(exc_type)
else:
_control_flow_exception_classes.add(exc_type)
def should_be_treated_as_error(ty: "Any", value: "Any") -> bool:
if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
# https://docs.python.org/3/library/exceptions.html#SystemExit
return False
if issubclass(ty, tuple(_control_flow_exception_classes)):
return False
return True
if TYPE_CHECKING:
T = TypeVar("T")
def try_convert(convert_func: "Callable[[Any], T]", value: "Any") -> "Optional[T]":
"""
Attempt to convert from an unknown type to a specific type, using the
given function. Return None if the conversion fails, i.e. if the function
raises an exception.
"""
try:
if isinstance(value, convert_func): # type: ignore
return value
except TypeError:
pass
try:
return convert_func(value)
except Exception:
return None
def safe_serialize(data: "Any") -> str:
"""Safely serialize to a readable string."""
def serialize_item(
item: "Any",
) -> "Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]":
if callable(item):
try:
module = getattr(item, "__module__", None)
qualname = getattr(item, "__qualname__", None)
name = getattr(item, "__name__", "anonymous")
if module and qualname:
full_path = f"{module}.{qualname}"
elif module and name:
full_path = f"{module}.{name}"
else:
full_path = name
return f""
except Exception:
return f""
elif isinstance(item, dict):
return {k: serialize_item(v) for k, v in item.items()}
elif isinstance(item, (list, tuple)):
return [serialize_item(x) for x in item]
elif hasattr(item, "__dict__"):
try:
attrs = {
k: serialize_item(v)
for k, v in vars(item).items()
if not k.startswith("_")
}
return f"<{type(item).__name__} {attrs}>"
except Exception:
return repr(item)
else:
return item
try:
serialized = serialize_item(data)
return (
json.dumps(serialized, default=str)
if not isinstance(serialized, str)
else serialized
)
except Exception:
return str(data)
def has_logs_enabled(options: "Optional[dict[str, Any]]") -> bool:
if options is None:
return False
return bool(
options.get("enable_logs", False)
or options["_experiments"].get("enable_logs", False)
)
def get_before_send_log(
options: "Optional[dict[str, Any]]",
) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
if options is None:
return None
return options.get("before_send_log") or options["_experiments"].get(
"before_send_log"
)
def has_metrics_enabled(options: "Optional[dict[str, Any]]") -> bool:
if options is None:
return False
return bool(options.get("enable_metrics", True))
def get_before_send_metric(
options: "Optional[dict[str, Any]]",
) -> "Optional[Callable[[Metric, Hint], Optional[Metric]]]":
if options is None:
return None
return options.get("before_send_metric") or options["_experiments"].get(
"before_send_metric"
)
def get_before_send_span(
options: "Optional[dict[str, Any]]",
) -> "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]":
if options is None:
return None
return options["_experiments"].get("before_send_span")
def format_attribute(val: "Any") -> "AttributeValue":
"""
Turn unsupported attribute value types into an AttributeValue.
We do this as soon as a user-provided attribute is set, to prevent spans,
logs, metrics and similar from having live references to various objects.
Note: This is not the final attribute value format. Before they're sent,
they're serialized further into the actual format the protocol expects:
https://develop.sentry.dev/sdk/telemetry/attributes/
"""
if isinstance(val, (bool, int, float, str)):
return val
if isinstance(val, (list, tuple)) and not val:
return []
elif isinstance(val, list):
ty = type(val[0])
if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
return copy.deepcopy(val)
elif isinstance(val, tuple):
ty = type(val[0])
if ty in (str, int, float, bool) and all(type(v) is ty for v in val):
return list(val)
return safe_repr(val)
def serialize_attribute(val: "AttributeValue") -> "SerializedAttributeValue":
"""Serialize attribute value to the transport format."""
if isinstance(val, bool):
return {"value": val, "type": "boolean"}
if isinstance(val, int):
return {"value": val, "type": "integer"}
if isinstance(val, float):
return {"value": val, "type": "double"}
if isinstance(val, str):
return {"value": val, "type": "string"}
if isinstance(val, list):
if not val:
return {"value": [], "type": "array"}
# Only lists of elements of a single type are supported
ty = type(val[0])
if ty in (int, str, bool, float) and all(type(v) is ty for v in val):
return {"value": val, "type": "array"}
# Coerce to string if we don't know what to do with the value. This should
# never happen as we pre-format early in format_attribute, but let's be safe.
return {"value": safe_repr(val), "type": "string"}
sentry-python-2.64.0/sentry_sdk/worker.py 0000664 0000000 0000000 00000025637 15220673775 0020551 0 ustar 00root root 0000000 0000000 import asyncio
import os
import threading
from abc import ABC, abstractmethod
from time import sleep, time
from typing import TYPE_CHECKING
from sentry_sdk._queue import FullError, Queue
from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
from sentry_sdk.utils import logger, mark_sentry_task_internal
if TYPE_CHECKING:
from typing import Any, Callable, Optional
_TERMINATOR = object()
class Worker(ABC):
"""Base class for all workers."""
@property
@abstractmethod
def is_alive(self) -> bool:
"""Whether the worker is alive and running."""
pass
@abstractmethod
def kill(self) -> None:
"""Kill the worker. It will not process any more events."""
pass
def flush(
self, timeout: float, callback: "Optional[Callable[[int, float], Any]]" = None
) -> None:
"""Flush the worker, blocking until done or timeout is reached."""
return None
@abstractmethod
def full(self) -> bool:
"""Whether the worker's queue is full."""
pass
@abstractmethod
def submit(self, callback: "Callable[[], Any]") -> bool:
"""Schedule a callback. Returns True if queued, False if full."""
pass
class BackgroundWorker(Worker):
def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
self._queue: "Queue" = Queue(queue_size)
self._lock = threading.Lock()
self._thread: "Optional[threading.Thread]" = None
self._thread_for_pid: "Optional[int]" = None
@property
def is_alive(self) -> bool:
if self._thread_for_pid != os.getpid():
return False
if not self._thread:
return False
return self._thread.is_alive()
def _ensure_thread(self) -> None:
if not self.is_alive:
self.start()
def _timed_queue_join(self, timeout: float) -> bool:
deadline = time() + timeout
queue = self._queue
queue.all_tasks_done.acquire()
try:
while queue.unfinished_tasks:
delay = deadline - time()
if delay <= 0:
return False
queue.all_tasks_done.wait(timeout=delay)
return True
finally:
queue.all_tasks_done.release()
def start(self) -> None:
with self._lock:
if not self.is_alive:
self._thread = threading.Thread(
target=self._target, name="sentry-sdk.BackgroundWorker"
)
self._thread.daemon = True
try:
self._thread.start()
self._thread_for_pid = os.getpid()
except RuntimeError:
# At this point we can no longer start because the interpreter
# is already shutting down. Sadly at this point we can no longer
# send out events.
self._thread = None
def kill(self) -> None:
"""
Kill worker thread. Returns immediately. Not useful for
waiting on shutdown for events, use `flush` for that.
"""
logger.debug("background worker got kill request")
with self._lock:
if self._thread:
try:
self._queue.put_nowait(_TERMINATOR)
except FullError:
logger.debug("background worker queue full, kill failed")
self._thread = None
self._thread_for_pid = None
def flush(self, timeout: float, callback: "Optional[Any]" = None) -> None:
logger.debug("background worker got flush request")
with self._lock:
if self.is_alive and timeout > 0.0:
self._wait_flush(timeout, callback)
logger.debug("background worker flushed")
def full(self) -> bool:
return self._queue.full()
def _wait_flush(self, timeout: float, callback: "Optional[Any]") -> None:
initial_timeout = min(0.1, timeout)
if not self._timed_queue_join(initial_timeout):
pending = self._queue.qsize() + 1
logger.debug("%d event(s) pending on flush", pending)
if callback is not None:
callback(pending, timeout)
if not self._timed_queue_join(timeout - initial_timeout):
pending = self._queue.qsize() + 1
logger.error("flush timed out, dropped %s events", pending)
def submit(self, callback: "Callable[[], Any]") -> bool:
self._ensure_thread()
try:
self._queue.put_nowait(callback)
return True
except FullError:
return False
def _target(self) -> None:
while True:
callback = self._queue.get()
try:
if callback is _TERMINATOR:
break
try:
callback()
except Exception:
logger.error("Failed processing job", exc_info=True)
finally:
self._queue.task_done()
sleep(0)
class AsyncWorker(Worker):
def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
self._queue: "Optional[asyncio.Queue[Any]]" = None
self._queue_size = queue_size
self._task: "Optional[asyncio.Task[None]]" = None
# Event loop needs to remain in the same process
self._task_for_pid: "Optional[int]" = None
self._loop: "Optional[asyncio.AbstractEventLoop]" = None
# Track active callback tasks so they have a strong reference and can be cancelled on kill
self._active_tasks: "set[asyncio.Task[None]]" = set()
@property
def is_alive(self) -> bool:
if self._task_for_pid != os.getpid():
return False
if not self._task or not self._loop:
return False
return self._loop.is_running() and not self._task.done()
def kill(self) -> None:
if self._task:
# Cancel the main consumer task to prevent duplicate consumers
self._task.cancel()
# Also cancel any active callback tasks
# Avoid modifying the set while cancelling tasks
tasks_to_cancel = set(self._active_tasks)
for task in tasks_to_cancel:
task.cancel()
self._active_tasks.clear()
self._loop = None
self._task = None
self._task_for_pid = None
def start(self) -> None:
if not self.is_alive:
try:
self._loop = asyncio.get_running_loop()
# Always create a fresh queue on start to avoid stale items
self._queue = asyncio.Queue(maxsize=self._queue_size)
with mark_sentry_task_internal():
self._task = self._loop.create_task(self._target())
self._task_for_pid = os.getpid()
except RuntimeError:
# There is no event loop running
logger.warning("No event loop running, async worker not started")
self._loop = None
self._task = None
self._task_for_pid = None
def full(self) -> bool:
if self._queue is None:
return True
return self._queue.full()
def _ensure_task(self) -> None:
if not self.is_alive:
self.start()
async def _wait_flush(
self, timeout: float, callback: "Optional[Any]" = None
) -> None:
if not self._loop or not self._loop.is_running() or self._queue is None:
return
initial_timeout = min(0.1, timeout)
# Timeout on the join
try:
await asyncio.wait_for(self._queue.join(), timeout=initial_timeout)
except asyncio.TimeoutError:
pending = self._queue.qsize() + len(self._active_tasks)
logger.debug("%d event(s) pending on flush", pending)
if callback is not None:
callback(pending, timeout)
try:
remaining_timeout = timeout - initial_timeout
await asyncio.wait_for(self._queue.join(), timeout=remaining_timeout)
except asyncio.TimeoutError:
pending = self._queue.qsize() + len(self._active_tasks)
logger.error("flush timed out, dropped %s events", pending)
def flush( # type: ignore[override]
self, timeout: float, callback: "Optional[Any]" = None
) -> "Optional[asyncio.Task[None]]":
if self.is_alive and timeout > 0.0 and self._loop and self._loop.is_running():
with mark_sentry_task_internal():
return self._loop.create_task(self._wait_flush(timeout, callback))
return None
def submit(self, callback: "Callable[[], Any]") -> bool:
self._ensure_task()
if self._queue is None:
return False
try:
self._queue.put_nowait(callback)
return True
except asyncio.QueueFull:
return False
async def _target(self) -> None:
if self._queue is None:
return
try:
while True:
callback = await self._queue.get()
if callback is _TERMINATOR:
self._queue.task_done()
break
# Firing tasks instead of awaiting them allows for concurrent requests
with mark_sentry_task_internal():
task = asyncio.create_task(self._process_callback(callback))
# Create a strong reference to the task so it can be cancelled on kill
# and does not get garbage collected while running
self._active_tasks.add(task)
# Capture queue ref at dispatch time so done callbacks use the
# correct queue even if kill()/start() replace self._queue.
queue_ref = self._queue
task.add_done_callback(lambda t: self._on_task_complete(t, queue_ref))
# Yield to let the event loop run other tasks
await asyncio.sleep(0)
except asyncio.CancelledError:
pass # Expected during kill()
async def _process_callback(self, callback: "Callable[[], Any]") -> None:
# Callback is an async coroutine, need to await it
await callback()
def _on_task_complete(
self,
task: "asyncio.Task[None]",
queue: "Optional[asyncio.Queue[Any]]" = None,
) -> None:
try:
task.result()
except asyncio.CancelledError:
pass # Task was cancelled, expected during shutdown
except Exception:
logger.error("Failed processing job", exc_info=True)
finally:
# Mark the task as done and remove it from the active tasks set
# Use the queue reference captured at dispatch time, not self._queue,
# to avoid calling task_done() on a different queue after kill()/start().
if queue is not None:
queue.task_done()
self._active_tasks.discard(task)
sentry-python-2.64.0/setup.py 0000664 0000000 0000000 00000010342 15220673775 0016176 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Sentry-Python - Sentry SDK for Python
=====================================
**Sentry-Python is an SDK for Sentry.** Check out `GitHub
`_ to find out more.
"""
import os
from setuptools import find_packages, setup
here = os.path.abspath(os.path.dirname(__file__))
def get_file_text(file_name):
with open(os.path.join(here, file_name)) as in_file:
return in_file.read()
setup(
name="sentry-sdk",
version="2.64.0",
author="Sentry Team and Contributors",
author_email="hello@sentry.io",
url="https://github.com/getsentry/sentry-python",
project_urls={
"Documentation": "https://docs.sentry.io/platforms/python/",
"Changelog": "https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md",
},
description="Python client for Sentry (https://sentry.io)",
long_description=get_file_text("README.md"),
long_description_content_type="text/markdown",
packages=find_packages(exclude=("tests", "tests.*")),
# PEP 561
package_data={"sentry_sdk": ["py.typed"]},
zip_safe=False,
license_expression="MIT",
python_requires=">=3.6",
install_requires=[
"urllib3>=1.26.11",
"certifi",
],
extras_require={
"aiohttp": ["aiohttp>=3.5"],
"anthropic": ["anthropic>=0.16"],
"arq": ["arq>=0.23"],
"asyncpg": ["asyncpg>=0.23"],
"beam": ["apache-beam>=2.12"],
"bottle": ["bottle>=0.12.13"],
"celery": ["celery>=3"],
"celery-redbeat": ["celery-redbeat>=2"],
"chalice": ["chalice>=1.16.0"],
"clickhouse-driver": ["clickhouse-driver>=0.2.0"],
"django": ["django>=1.8"],
"falcon": ["falcon>=1.4"],
"fastapi": ["fastapi>=0.79.0"],
"flask": ["flask>=0.11", "blinker>=1.1", "markupsafe"],
"grpcio": ["grpcio>=1.21.1", "protobuf>=3.8.0"],
"http2": ["httpcore[http2]==1.*"],
"asyncio": ["httpcore[asyncio]==1.*"],
"httpx": ["httpx>=0.16.0"],
"huey": ["huey>=2"],
"huggingface_hub": ["huggingface_hub>=0.22"],
"langchain": ["langchain>=0.0.210"],
"langgraph": ["langgraph>=0.6.6"],
"launchdarkly": ["launchdarkly-server-sdk>=9.8.0"],
"litellm": ["litellm>=1.77.5,!=1.82.7,!=1.82.8"],
"litestar": ["litestar>=2.0.0"],
"loguru": ["loguru>=0.5"],
"mcp": ["mcp>=1.15.0"],
"openai": ["openai>=1.0.0", "tiktoken>=0.3.0"],
"openfeature": ["openfeature-sdk>=0.7.1"],
"opentelemetry": ["opentelemetry-distro>=0.35b0"],
"opentelemetry-experimental": ["opentelemetry-distro"],
"opentelemetry-otlp": ["opentelemetry-distro[otlp]>=0.35b0"],
"pure-eval": ["pure_eval", "executing", "asttokens"],
"pydantic_ai": ["pydantic-ai>=1.0.0"],
"pymongo": ["pymongo>=3.1"],
"pyspark": ["pyspark>=2.4.4"],
"quart": ["quart>=0.16.1", "blinker>=1.1"],
"rq": ["rq>=0.6"],
"sanic": ["sanic>=0.8"],
"sqlalchemy": ["sqlalchemy>=1.2"],
"starlette": ["starlette>=0.19.1"],
"starlite": ["starlite>=1.48"],
"statsig": ["statsig>=0.55.3"],
"tornado": ["tornado>=6"],
"unleash": ["UnleashClient>=6.0.1"],
"google-genai": ["google-genai>=1.29.0"],
},
entry_points={
"opentelemetry_propagator": [
"sentry=sentry_sdk.integrations.opentelemetry:SentryPropagator"
]
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Libraries :: Python Modules",
],
)
sentry-python-2.64.0/tests/ 0000775 0000000 0000000 00000000000 15220673775 0015626 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/__init__.py 0000664 0000000 0000000 00000000661 15220673775 0017742 0 ustar 00root root 0000000 0000000 import sys
import warnings
# This is used in _capture_internal_warnings. We need to run this at import
# time because that's where many deprecation warnings might get thrown.
#
# This lives in tests/__init__.py because apparently even tests/conftest.py
# gets loaded too late.
assert "sentry_sdk" not in sys.modules
_warning_recorder_mgr = warnings.catch_warnings(record=True)
_warning_recorder = _warning_recorder_mgr.__enter__()
sentry-python-2.64.0/tests/conftest.py 0000664 0000000 0000000 00000143530 15220673775 0020033 0 ustar 00root root 0000000 0000000 import asyncio
import gzip
import io
import json
import os
import socket
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:
import brotli
except ImportError:
brotli = None
try:
import jsonschema
except ImportError:
jsonschema = None
import pytest
try:
from pytest_localserver.http import WSGIServer
except ImportError:
WSGIServer = None
try:
from werkzeug.wrappers import Request, Response
except ImportError:
Request = None
Response = None
try:
from starlette.testclient import TestClient
# Catch RuntimeError to prevent the following exception in aws_lambda tests.
# RuntimeError: The starlette.testclient module requires the httpx package to be installed.
except (ImportError, RuntimeError):
TestClient = None
try:
import gevent
except ImportError:
gevent = None
try:
import eventlet
except ImportError:
eventlet = None
import sentry_sdk
import sentry_sdk.utils
from sentry_sdk.envelope import Envelope, parse_json
from sentry_sdk.integrations import ( # noqa: F401
_DEFAULT_INTEGRATIONS,
_installed_integrations,
_processed_integrations,
)
from sentry_sdk.profiler import teardown_profiler
from sentry_sdk.profiler.continuous_profiler import teardown_continuous_profiler
from sentry_sdk.transport import Transport
from sentry_sdk.utils import package_version, reraise
try:
import openai
except ImportError:
openai = None
try:
import anthropic
except ImportError:
anthropic = None
try:
import google
except ImportError:
google = None
from typing import TYPE_CHECKING
from tests import _warning_recorder, _warning_recorder_mgr
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Any, Callable, MutableMapping, Optional
try:
from httpx import (
ASGITransport,
AsyncByteStream,
AsyncClient,
)
from httpx import (
Request as HttpxRequest,
)
from httpx import (
Response as HttpxResponse,
)
except ImportError:
ASGITransport = None
HttpxRequest = None
HttpxResponse = None
AsyncByteStream = None
AsyncClient = None
try:
from anyio import EndOfStream, create_memory_object_stream, create_task_group
from mcp.shared.message import SessionMessage
from mcp.types import (
JSONRPCMessage,
JSONRPCNotification,
JSONRPCRequest,
)
_MCP_VERSION = package_version("mcp")
_IS_MCP_V2 = _MCP_VERSION is not None and _MCP_VERSION >= (2, 0, 0)
except ImportError:
create_memory_object_stream = None
create_task_group = None
EndOfStream = None
JSONRPCMessage = None
JSONRPCNotification = None
JSONRPCRequest = None
SessionMessage = None
_IS_MCP_V2 = False
SENTRY_EVENT_SCHEMA = "./checkouts/data-schemas/relay/event.schema.json"
if not os.path.isfile(SENTRY_EVENT_SCHEMA):
SENTRY_EVENT_SCHEMA = None
else:
with open(SENTRY_EVENT_SCHEMA) as f:
SENTRY_EVENT_SCHEMA = json.load(f)
from sentry_sdk import scope
@pytest.fixture(autouse=True)
def clean_scopes():
"""
Resets the scopes for every test to avoid leaking data between tests.
"""
scope._global_scope = None
scope._isolation_scope.set(None)
scope._current_scope.set(None)
@pytest.fixture(autouse=True)
def internal_exceptions(request):
errors = []
if "tests_internal_exceptions" in request.keywords:
return
def _capture_internal_exception(exc_info):
errors.append(exc_info)
@request.addfinalizer
def _():
# reraise the errors so that this just acts as a pass-through (that
# happens to keep track of the errors which pass through it)
for e in errors:
reraise(*e)
sentry_sdk.utils.capture_internal_exception = _capture_internal_exception
return errors
@pytest.fixture(autouse=True, scope="session")
def _capture_internal_warnings():
yield
_warning_recorder_mgr.__exit__(None, None, None)
recorder = _warning_recorder
for warning in recorder:
try:
if isinstance(warning.message, ResourceWarning):
continue
except NameError:
pass
if "sentry_sdk" not in str(warning.filename) and "sentry-sdk" not in str(
warning.filename
):
continue
# pytest-django
if "getfuncargvalue" in str(warning.message):
continue
# Happens when re-initializing the SDK
if "but it was only enabled on init()" in str(warning.message):
continue
# sanic's usage of aiohttp for test client
if "verify_ssl is deprecated, use ssl=False instead" in str(warning.message):
continue
if "getargspec" in str(warning.message) and warning.filename.endswith(
("pyramid/config/util.py", "pyramid/config/views.py")
):
continue
if "isAlive() is deprecated" in str(
warning.message
) and warning.filename.endswith("celery/utils/timer2.py"):
continue
if "collections.abc" in str(warning.message) and warning.filename.endswith(
("celery/canvas.py", "werkzeug/datastructures.py", "tornado/httputil.py")
):
continue
# Django 1.7 emits a (seemingly) false-positive warning for our test
# app and suggests to use a middleware that does not exist in later
# Django versions.
if "SessionAuthenticationMiddleware" in str(warning.message):
continue
if "Something has already installed a non-asyncio" in str(warning.message):
continue
if "dns.hash" in str(warning.message) or "dns/namedict" in warning.filename:
continue
# On Python < 3.8 we fall back to importing `pkg_resources` to
# enumerate installed modules, which emits a DeprecationWarning.
if "pkg_resources is deprecated as an API" in str(
warning.message
) and warning.filename.endswith("sentry_sdk/utils.py"):
continue
raise AssertionError(warning)
@pytest.fixture
def validate_event_schema(tmpdir):
def inner(event):
assert jsonschema is not None
if SENTRY_EVENT_SCHEMA:
jsonschema.validate(instance=event, schema=SENTRY_EVENT_SCHEMA)
return inner
@pytest.fixture
def reset_integrations():
"""
Use with caution, sometimes we really need to start
with a clean slate to ensure monkeypatching works well,
but this also means some other stuff will be monkeypatched twice.
"""
global _DEFAULT_INTEGRATIONS, _processed_integrations
try:
_DEFAULT_INTEGRATIONS.remove(
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration"
)
except ValueError:
pass
_processed_integrations.clear()
_installed_integrations.clear()
@pytest.fixture
def uninstall_integration():
"""Use to force the next call to sentry_init to re-install/setup an integration."""
def inner(identifier):
_processed_integrations.discard(identifier)
_installed_integrations.discard(identifier)
return inner
@pytest.fixture
def sentry_init(request):
def inner(*a, **kw):
kw.setdefault("transport", TestTransport())
client = sentry_sdk.Client(*a, **kw)
sentry_sdk.get_global_scope().set_client(client)
if request.node.get_closest_marker("forked"):
# Do not run isolation if the test is already running in
# ultimate isolation (seems to be required for celery tests that
# fork)
yield inner
else:
old_client = sentry_sdk.get_global_scope().client
try:
sentry_sdk.get_current_scope().set_client(None)
yield inner
finally:
sentry_sdk.get_global_scope().set_client(old_client)
class TestTransport(Transport):
def __init__(self):
Transport.__init__(self)
def capture_envelope(self, _: Envelope) -> None:
"""No-op capture_envelope for tests"""
pass
class TestTransportWithOptions(Transport):
"""TestTransport above does not pass in the options and for some tests we need them"""
__test__ = False
def __init__(self, options=None):
Transport.__init__(self, options)
def capture_envelope(self, _: Envelope) -> None:
"""No-op capture_envelope for tests"""
pass
@pytest.fixture
def capture_events(monkeypatch):
def inner():
events = []
test_client = sentry_sdk.get_client()
old_capture_envelope = test_client.transport.capture_envelope
def append_event(envelope):
for item in envelope:
if item.headers.get("type") in ("event", "transaction"):
events.append(item.payload.json)
return old_capture_envelope(envelope)
monkeypatch.setattr(test_client.transport, "capture_envelope", append_event)
return events
return inner
@pytest.fixture
def capture_envelopes(monkeypatch):
def inner():
envelopes = []
test_client = sentry_sdk.get_client()
old_capture_envelope = test_client.transport.capture_envelope
def append_envelope(envelope):
envelopes.append(envelope)
return old_capture_envelope(envelope)
monkeypatch.setattr(test_client.transport, "capture_envelope", append_envelope)
return envelopes
return inner
@dataclass
class UnwrappedItem:
type: str
payload: dict
@pytest.fixture
def capture_items(monkeypatch):
"""
Capture envelope payload, unfurling individual items.
Makes it easier to work with both events and attribute-based telemetry in
one test.
"""
def inner(*types):
telemetry = []
test_client = sentry_sdk.get_client()
old_capture_envelope = test_client.transport.capture_envelope
def append_envelope(envelope):
for item in envelope:
if types and item.type not in types:
continue
if item.type in ("trace_metric", "log", "span"):
for i in item.payload.json["items"]:
t = {k: v for k, v in i.items() if k != "attributes"}
t["attributes"] = {
k: v["value"] for k, v in i["attributes"].items()
}
telemetry.append(UnwrappedItem(type=item.type, payload=t))
else:
telemetry.append(
UnwrappedItem(type=item.type, payload=item.payload.json)
)
return old_capture_envelope(envelope)
monkeypatch.setattr(test_client.transport, "capture_envelope", append_envelope)
return telemetry
return inner
@pytest.fixture
def capture_record_lost_event_calls(monkeypatch):
def inner():
calls = []
test_client = sentry_sdk.get_client()
def record_lost_event(reason, data_category=None, item=None, *, quantity=1):
calls.append((reason, data_category, item, quantity))
monkeypatch.setattr(
test_client.transport, "record_lost_event", record_lost_event
)
return calls
return inner
@pytest.fixture
def capture_events_forksafe(monkeypatch, capture_events, request):
def inner():
capture_events()
events_r, events_w = os.pipe()
events_r = os.fdopen(events_r, "rb", 0)
events_w = os.fdopen(events_w, "wb", 0)
test_client = sentry_sdk.get_client()
old_capture_envelope = test_client.transport.capture_envelope
def append(envelope):
event = envelope.get_event() or envelope.get_transaction_event()
if event is not None:
events_w.write(json.dumps(event).encode("utf-8"))
events_w.write(b"\n")
return old_capture_envelope(envelope)
def flush(timeout=None, callback=None):
events_w.write(b"flush\n")
monkeypatch.setattr(test_client.transport, "capture_envelope", append)
monkeypatch.setattr(test_client, "flush", flush)
return EventStreamReader(events_r, events_w)
return inner
@pytest.fixture
def capture_items_forksafe(monkeypatch, capture_items, request):
def inner(*types):
capture_items(*types)
items_r, items_w = os.pipe()
items_r = os.fdopen(items_r, "rb", 0)
items_w = os.fdopen(items_w, "wb", 0)
test_client = sentry_sdk.get_client()
old_capture_envelope = test_client.transport.capture_envelope
telemetry = []
def append(envelope):
for item in envelope:
if types and item.type not in types:
continue
if item.type in ("metric", "log", "span"):
for i in item.payload.json["items"]:
t = {k: v for k, v in i.items() if k != "attributes"}
t["attributes"] = {
k: v["value"] for k, v in i["attributes"].items()
}
telemetry.append({"type": item.type, "payload": t})
else:
telemetry.append({"type": item.type, "payload": item.payload.json})
return old_capture_envelope(envelope)
real_flush = test_client.flush
def flush(timeout=None, callback=None):
real_flush(timeout=timeout, callback=callback)
items_w.write(json.dumps(telemetry).encode("utf-8") + b"\n")
items_w.write(b"flush\n")
monkeypatch.setattr(test_client.transport, "capture_envelope", append)
monkeypatch.setattr(test_client, "flush", flush)
return EventStreamReader(items_r, items_w)
return inner
class EventStreamReader:
def __init__(self, read_file, write_file):
self.read_file = read_file
self.write_file = write_file
def read_event(self):
return json.loads(self.read_file.readline().decode("utf-8"))
def read_flush(self):
assert self.read_file.readline() == b"flush\n"
# scope=session ensures that fixture is run earlier
@pytest.fixture(
scope="session",
params=[None, "eventlet", "gevent"],
ids=("threads", "eventlet", "greenlet"),
)
def maybe_monkeypatched_threading(request):
if request.param == "eventlet":
if eventlet is None:
pytest.skip("no eventlet installed")
try:
eventlet.monkey_patch()
except AttributeError as e:
if "'thread.RLock' object has no attribute" in str(e):
# https://bitbucket.org/pypy/pypy/issues/2962/gevent-cannot-patch-rlock-under-pypy-27-7
pytest.skip("https://github.com/eventlet/eventlet/issues/546")
else:
raise
elif request.param == "gevent":
if gevent is None:
pytest.skip("no gevent installed")
try:
gevent.monkey.patch_all()
except Exception as e:
if "_RLock__owner" in str(e):
pytest.skip("https://github.com/gevent/gevent/issues/1380")
else:
raise
else:
assert request.param is None
return request.param
@pytest.fixture
def render_span_tree():
def inner(spans, root_span=None):
streamed_spans = False
if root_span is None:
streamed_spans = True
by_parent = {}
for span in spans:
if "parent_span_id" not in span:
root_span = span
continue
by_parent.setdefault(span["parent_span_id"], []).append(span)
def render_span(span):
if streamed_spans:
yield "- sentry.op={}: name={}".format(
json.dumps(span["attributes"].get("sentry.op")),
json.dumps(span["name"]),
)
else:
yield "- op={}: description={}".format(
json.dumps(span.get("op")), json.dumps(span.get("description"))
)
for subspan in by_parent.get(span["span_id"]) or ():
for line in render_span(subspan):
yield " {}".format(line)
return "\n".join(render_span(root_span))
return inner
@pytest.fixture(name="StringContaining")
def string_containing_matcher():
"""
An object which matches any string containing the substring passed to the
object at instantiation time.
Useful for assert_called_with, assert_any_call, etc.
Used like this:
>>> f = mock.Mock()
>>> f("dogs are great")
>>> f.assert_any_call("dogs") # will raise AssertionError
Traceback (most recent call last):
...
AssertionError: mock('dogs') call not found
>>> f.assert_any_call(StringContaining("dogs")) # no AssertionError
"""
class StringContaining:
def __init__(self, substring):
self.substring = substring
self.valid_types = (str, bytes)
def __eq__(self, test_string):
if not isinstance(test_string, self.valid_types):
return False
# this is safe even in py2 because as of 2.6, `bytes` exists in py2
# as an alias for `str`
if isinstance(test_string, bytes):
test_string = test_string.decode()
if len(self.substring) > len(test_string):
return False
return self.substring in test_string
def __ne__(self, test_string):
return not self.__eq__(test_string)
return StringContaining
def _safe_is_equal(x, y):
"""
Compares two values, preferring to use the first's __eq__ method if it
exists and is implemented.
Accounts for py2/py3 differences (like ints in py2 not having a __eq__
method), as well as the incomparability of certain types exposed by using
raw __eq__ () rather than ==.
"""
# Prefer using __eq__ directly to ensure that examples like
#
# maisey = Dog()
# maisey.name = "Maisey the Dog"
# maisey == ObjectDescribedBy(attrs={"name": StringContaining("Maisey")})
#
# evaluate to True (in other words, examples where the values in self.attrs
# might also have custom __eq__ methods; this makes sure those methods get
# used if possible)
try:
is_equal = x.__eq__(y)
except AttributeError:
is_equal = NotImplemented
# this can happen on its own, too (i.e. without an AttributeError being
# thrown), which is why this is separate from the except block above
if is_equal == NotImplemented:
# using == smoothes out weird variations exposed by raw __eq__
return x == y
return is_equal
@pytest.fixture(name="DictionaryContaining")
def dictionary_containing_matcher():
"""
An object which matches any dictionary containing all key-value pairs from
the dictionary passed to the object at instantiation time.
Useful for assert_called_with, assert_any_call, etc.
Used like this:
>>> f = mock.Mock()
>>> f({"dogs": "yes", "cats": "maybe"})
>>> f.assert_any_call({"dogs": "yes"}) # will raise AssertionError
Traceback (most recent call last):
...
AssertionError: mock({'dogs': 'yes'}) call not found
>>> f.assert_any_call(DictionaryContaining({"dogs": "yes"})) # no AssertionError
"""
class DictionaryContaining:
def __init__(self, subdict):
self.subdict = subdict
def __eq__(self, test_dict):
if not isinstance(test_dict, dict):
return False
if len(self.subdict) > len(test_dict):
return False
for key, value in self.subdict.items():
try:
test_value = test_dict[key]
except KeyError: # missing key
return False
if not _safe_is_equal(value, test_value):
return False
return True
def __ne__(self, test_dict):
return not self.__eq__(test_dict)
return DictionaryContaining
@pytest.fixture(name="ObjectDescribedBy")
def object_described_by_matcher():
"""
An object which matches any other object with the given properties.
Available properties currently are "type" (a type object) and "attrs" (a
dictionary).
Useful for assert_called_with, assert_any_call, etc.
Used like this:
>>> class Dog:
... pass
...
>>> maisey = Dog()
>>> maisey.name = "Maisey"
>>> maisey.age = 7
>>> f = mock.Mock()
>>> f(maisey)
>>> f.assert_any_call(ObjectDescribedBy(type=Dog)) # no AssertionError
>>> f.assert_any_call(ObjectDescribedBy(attrs={"name": "Maisey"})) # no AssertionError
"""
class ObjectDescribedBy:
def __init__(self, type=None, attrs=None):
self.type = type
self.attrs = attrs
def __eq__(self, test_obj):
if self.type:
if not isinstance(test_obj, self.type):
return False
if self.attrs:
for attr_name, attr_value in self.attrs.items():
try:
test_value = getattr(test_obj, attr_name)
except AttributeError: # missing attribute
return False
if not _safe_is_equal(attr_value, test_value):
return False
return True
def __ne__(self, test_obj):
return not self.__eq__(test_obj)
return ObjectDescribedBy
@pytest.fixture
def teardown_profiling():
# Make sure that a previous test didn't leave the profiler running
teardown_profiler()
teardown_continuous_profiler()
yield
# Make sure that to shut down the profiler after the test
teardown_profiler()
teardown_continuous_profiler()
@pytest.fixture()
def suppress_deprecation_warnings():
"""
Use this fixture to suppress deprecation warnings in a test.
Useful for testing deprecated SDK features.
"""
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
yield
def _make_session_message(jsonrpc_msg):
"""Construct a SessionMessage compatible with both MCP v1 and v2."""
if _IS_MCP_V2:
return SessionMessage(message=jsonrpc_msg) # type: ignore
else:
return SessionMessage(message=JSONRPCMessage(root=jsonrpc_msg)) # type: ignore
@pytest.fixture
def get_initialization_payload():
def inner(request_id: str):
return _make_session_message(
JSONRPCRequest( # type: ignore
jsonrpc="2.0",
id=request_id,
method="initialize",
params={
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0.0"},
},
)
)
return inner
@pytest.fixture
def get_initialized_notification_payload():
def inner():
return _make_session_message(
JSONRPCNotification( # type: ignore
jsonrpc="2.0",
method="notifications/initialized",
)
)
return inner
@pytest.fixture
def get_mcp_command_payload():
def inner(method: str, params, request_id: str):
return _make_session_message(
JSONRPCRequest( # type: ignore
jsonrpc="2.0",
id=request_id,
method=method,
params=params,
)
)
return inner
@pytest.fixture
def stdio(
get_initialization_payload,
get_initialized_notification_payload,
get_mcp_command_payload,
):
async def inner(server, method: str, params, request_id: str | None = None):
if request_id is None:
request_id = "1"
read_stream_writer, read_stream = create_memory_object_stream(0) # type: ignore
write_stream, write_stream_reader = create_memory_object_stream(0) # type: ignore
result = {}
async def run_server():
await server.run(
read_stream, write_stream, server.create_initialization_options()
)
async def simulate_client(tg, result):
init_request = get_initialization_payload("1")
await read_stream_writer.send(init_request)
await write_stream_reader.receive()
initialized_notification = get_initialized_notification_payload()
await read_stream_writer.send(initialized_notification)
request = get_mcp_command_payload(
method, params=params, request_id=request_id
)
await read_stream_writer.send(request)
result["response"] = await write_stream_reader.receive()
tg.cancel_scope.cancel()
async with create_task_group() as tg: # type: ignore
tg.start_soon(run_server)
tg.start_soon(simulate_client, tg, result)
return result["response"]
return inner
@pytest.fixture()
def json_rpc():
def inner(app, method: str, params, request_id: str):
with TestClient(app) as client: # type: ignore
init_response = client.post(
"/mcp/",
headers={
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
},
json={
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"clientInfo": {"name": "test-client", "version": "1.0"},
"protocolVersion": "2025-11-25",
"capabilities": {},
},
"id": request_id,
},
)
session_id = init_response.headers["mcp-session-id"]
# Notification response is mandatory.
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
client.post(
"/mcp/",
headers={
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
"mcp-session-id": session_id,
},
json={
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {},
},
)
response = client.post(
"/mcp/",
headers={
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
"mcp-session-id": session_id,
},
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": request_id,
},
)
return session_id, response
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["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):
"""
Simple transport whose only purpose is to keep GET request alive in SSE connections, allowing
tests involving SSE interactions to run in-process.
"""
def __init__(
self,
app: "Callable",
keep_sse_alive: "asyncio.Event",
) -> None:
self.keep_sse_alive = keep_sse_alive
super().__init__(app)
async def handle_async_request(
self, request: "HttpxRequest"
) -> "HttpxResponse":
scope = {
"type": "http",
"method": request.method,
"headers": [(k.lower(), v) for (k, v) in request.headers.raw],
"path": request.url.path,
"query_string": request.url.query,
}
is_streaming_sse = scope["method"] == "GET" and scope["path"] == "/sse"
if not is_streaming_sse:
return await super().handle_async_request(request)
request_body = b""
if request.content:
request_body = await request.aread()
body_sender, body_receiver = create_memory_object_stream[bytes](0) # type: ignore
async def receive() -> "dict[str, Any]":
if self.keep_sse_alive.is_set():
return {"type": "http.disconnect"}
await self.keep_sse_alive.wait() # Keep alive :)
return {
"type": "http.request",
"body": request_body,
"more_body": False,
}
async def send(message: "MutableMapping[str, Any]") -> None:
if message["type"] == "http.response.body":
body = message.get("body", b"")
more_body = message.get("more_body", False)
if body == b"" and not more_body:
return
if body:
await body_sender.send(body)
if not more_body:
await body_sender.aclose()
async def run_app():
await self.app(scope, receive, send)
class StreamingBodyStream(AsyncByteStream): # type: ignore
def __init__(self, receiver):
self.receiver = receiver
async def __aiter__(self):
try:
async for chunk in self.receiver:
yield chunk
except EndOfStream: # type: ignore
pass
stream = StreamingBodyStream(body_receiver)
response = HttpxResponse(status_code=200, headers=[], stream=stream) # type: ignore
asyncio.create_task(run_app())
return response
def parse_sse_data_package(sse_chunk):
sse_text = sse_chunk.decode("utf-8")
json_str = sse_text.split("data: ")[1]
return json.loads(json_str)
async def inner(
app, method: str, params, request_id: str, keep_sse_alive: "asyncio.Event"
):
context = {}
stream_complete = asyncio.Event()
endpoint_parsed = asyncio.Event()
# https://github.com/Kludex/starlette/issues/104#issuecomment-729087925
async with AsyncClient( # type: ignore
transport=StreamingASGITransport(app=app, keep_sse_alive=keep_sse_alive),
base_url="http://test",
) as client:
async def parse_stream():
async with client.stream("GET", "/sse") as stream:
# Read directly from stream.stream instead of aiter_bytes()
async for chunk in stream.stream:
if b"event: endpoint" in chunk:
sse_text = chunk.decode("utf-8")
url = sse_text.split("data: ")[1]
parsed = urlparse(url)
query_params = parse_qs(parsed.query)
context["session_id"] = query_params["session_id"][0]
endpoint_parsed.set()
continue
if b"event: message" in chunk and b"structuredContent" in chunk:
context["response"] = parse_sse_data_package(chunk)
break
elif (
"result" in parse_sse_data_package(chunk)
and "content" in parse_sse_data_package(chunk)["result"]
):
context["response"] = parse_sse_data_package(chunk)
break
stream_complete.set()
task = asyncio.create_task(parse_stream())
await endpoint_parsed.wait()
await client.post(
f"/messages/?session_id={context['session_id']}",
headers={
"Content-Type": "application/json",
},
json={
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"clientInfo": {"name": "test-client", "version": "1.0"},
"protocolVersion": "2025-11-25",
"capabilities": {},
},
"id": request_id,
},
)
# Notification response is mandatory.
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle
await client.post(
f"/messages/?session_id={context['session_id']}",
headers={
"Content-Type": "application/json",
"mcp-session-id": context["session_id"],
},
json={
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {},
},
)
await client.post(
f"/messages/?session_id={context['session_id']}",
headers={
"Content-Type": "application/json",
"mcp-session-id": context["session_id"],
},
json={
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": request_id,
},
)
await stream_complete.wait()
keep_sse_alive.set()
return task, context["session_id"], context["response"]
return inner
@pytest.fixture()
def async_iterator():
async def inner(values):
for value in values:
yield value
return inner
@pytest.fixture
def server_side_event_chunks():
def inner(events, include_event_type=True):
for event in events:
payload = event.model_dump()
chunk = (
f"event: {payload['type']}\ndata: {json.dumps(payload)}\n\n"
if include_event_type
else f"data: {json.dumps(payload)}\n\n"
)
yield chunk.encode("utf-8")
return inner
@pytest.fixture
def get_model_response():
def inner(response_content, serialize_pydantic=False, request_headers=None):
if request_headers is None:
request_headers = {}
model_request = HttpxRequest(
"POST",
"/responses",
headers=request_headers,
)
if serialize_pydantic:
response_content = json.dumps(
response_content.model_dump(
by_alias=True,
exclude_none=True,
)
).encode("utf-8")
response = HttpxResponse(
200,
request=model_request,
content=response_content,
)
return response
return inner
@pytest.fixture
def get_rate_limit_model_response():
def inner(request_headers=None):
if request_headers is None:
request_headers = {}
model_request = HttpxRequest(
"POST",
"/responses",
headers=request_headers,
)
response = HttpxResponse(
429,
request=model_request,
)
return response
return inner
@pytest.fixture
def streaming_chat_completions_model_response():
return [
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
role="assistant"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
content="Tes"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
content="t r"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
content="esp"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
content="ons"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(
content="e"
),
finish_reason=None,
),
],
),
openai.types.chat.ChatCompletionChunk(
id="chatcmpl-test",
object="chat.completion.chunk",
created=10000000,
model="gpt-3.5-turbo",
choices=[
openai.types.chat.chat_completion_chunk.Choice(
index=0,
delta=openai.types.chat.chat_completion_chunk.ChoiceDelta(),
finish_reason="stop",
),
],
usage=openai.types.CompletionUsage(
prompt_tokens=10,
completion_tokens=20,
total_tokens=30,
),
),
]
@pytest.fixture
def nonstreaming_chat_completions_model_response():
def inner(
response_id: str,
response_model: str,
message_content: str,
created: int,
usage: openai.types.CompletionUsage,
):
return openai.types.chat.ChatCompletion(
id=response_id,
choices=[
openai.types.chat.chat_completion.Choice(
index=0,
finish_reason="stop",
message=openai.types.chat.ChatCompletionMessage(
role="assistant", content=message_content
),
)
],
created=created,
model=response_model,
object="chat.completion",
usage=usage,
)
return inner
@pytest.fixture
def openai_embedding_model_response():
return openai.types.CreateEmbeddingResponse(
data=[
openai.types.Embedding(
embedding=[0.1, 0.2, 0.3],
index=0,
object="embedding",
)
],
model="text-embedding-ada-002",
object="list",
usage=openai.types.create_embedding_response.Usage(
prompt_tokens=5,
total_tokens=5,
),
)
@pytest.fixture
def nonstreaming_responses_model_response():
return openai.types.responses.Response(
id="resp_123",
output=[
openai.types.responses.ResponseOutputMessage(
id="msg_123",
type="message",
status="completed",
content=[
openai.types.responses.ResponseOutputText(
text="Hello, how can I help you?",
type="output_text",
annotations=[],
)
],
role="assistant",
)
],
parallel_tool_calls=False,
tool_choice="none",
tools=[],
created_at=10000000,
model="gpt-4",
object="response",
usage=openai.types.responses.ResponseUsage(
input_tokens=10,
input_tokens_details=openai.types.responses.response_usage.InputTokensDetails(
cached_tokens=0,
),
output_tokens=20,
output_tokens_details=openai.types.responses.response_usage.OutputTokensDetails(
reasoning_tokens=5,
),
total_tokens=30,
),
)
@pytest.fixture
def nonstreaming_anthropic_model_response():
return anthropic.types.Message(
id="msg_123",
type="message",
role="assistant",
model="claude-3-opus-20240229",
content=[
anthropic.types.TextBlock(
type="text",
text="Hello, how can I help you?",
)
],
stop_reason="end_turn",
stop_sequence=None,
usage=anthropic.types.Usage(
input_tokens=10,
output_tokens=20,
),
)
@pytest.fixture
def nonstreaming_google_genai_model_response():
return google.genai.types.GenerateContentResponse(
response_id="resp_123",
candidates=[
google.genai.types.Candidate(
content=google.genai.types.Content(
role="model",
parts=[
google.genai.types.Part(
text="Hello, how can I help you?",
)
],
),
finish_reason="STOP",
)
],
model_version="gemini/gemini-pro",
usage_metadata=google.genai.types.GenerateContentResponseUsageMetadata(
prompt_token_count=10,
candidates_token_count=20,
total_token_count=30,
),
)
@pytest.fixture
def responses_tool_call_model_responses():
def inner(
tool_name: str,
arguments: str,
response_model: str,
response_text: str,
response_ids: "Iterator[str]",
usages: "Iterator[openai.types.responses.ResponseUsage]",
):
yield openai.types.responses.Response(
id=next(response_ids),
output=[
openai.types.responses.ResponseFunctionToolCall(
id="call_123",
call_id="call_123",
name=tool_name,
type="function_call",
arguments=arguments,
)
],
parallel_tool_calls=False,
tool_choice="none",
tools=[],
created_at=10000000,
model=response_model,
object="response",
usage=next(usages),
)
yield openai.types.responses.Response(
id=next(response_ids),
output=[
openai.types.responses.ResponseOutputMessage(
id="msg_final",
type="message",
status="completed",
content=[
openai.types.responses.ResponseOutputText(
text=response_text,
type="output_text",
annotations=[],
)
],
role="assistant",
)
],
parallel_tool_calls=False,
tool_choice="none",
tools=[],
created_at=10000000,
model=response_model,
object="response",
usage=next(usages),
)
return inner
class MockServerRequestHandler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802
# Process an HTTP GET request and return a response.
# If the path ends with /status/, return status code .
# Otherwise return a 200 response.
code = 200
if "/status/" in self.path:
code = int(self.path[-3:])
self.send_response(code)
self.end_headers()
return
def get_free_port():
s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM)
s.bind(("localhost", 0))
_, port = s.getsockname()
s.close()
return port
def create_mock_http_server():
# Start a mock server to test outgoing http requests
mock_server_port = get_free_port()
mock_server = HTTPServer(("localhost", mock_server_port), MockServerRequestHandler)
mock_server_thread = Thread(target=mock_server.serve_forever)
mock_server_thread.daemon = True
mock_server_thread.start()
return mock_server_port
def unpack_werkzeug_response(response):
# werkzeug < 2.1 returns a tuple as client response, newer versions return
# an object
try:
return response.get_data(), response.status, response.headers
except AttributeError:
content, status, headers = response
return b"".join(content), status, headers
def werkzeug_set_cookie(client, servername, key, value):
# client.set_cookie has a different signature in different werkzeug versions
try:
client.set_cookie(servername, key, value)
except TypeError:
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
# all the keys from the ApproxDict with the same values.
#
# The other dict may contain additional keys with any value.
return all(key in other and other[key] == value for key, value in self.items())
def __ne__(self, other):
return not self.__eq__(other)
CapturedData = namedtuple("CapturedData", ["path", "event", "envelope", "compressed"])
@pytest.fixture(scope="module")
def wsgi_capturing_server():
assert WSGIServer is not None
class CapturingServer(WSGIServer):
def __init__(self, host="127.0.0.1", port=0, ssl_context=None):
WSGIServer.__init__(self, host, port, self, ssl_context=ssl_context)
self.code = 204
self.headers = {}
self.captured = []
def respond_with(self, code=200, headers=None):
self.code = code
if headers:
self.headers = headers
def clear_captured(self):
del self.captured[:]
def __call__(self, environ, start_response):
"""
This is the WSGI application.
"""
assert Request is not None
assert Response is not None
request = Request(environ)
event = envelope = None
content_encoding = request.headers.get("content-encoding")
if content_encoding == "gzip":
rdr = gzip.GzipFile(fileobj=io.BytesIO(request.data))
compressed = True
elif content_encoding == "br":
rdr = io.BytesIO(brotli.decompress(request.data))
compressed = True
else:
rdr = io.BytesIO(request.data)
compressed = False
if request.mimetype == "application/json":
event = parse_json(rdr.read())
else:
envelope = Envelope.deserialize_from(rdr)
self.captured.append(
CapturedData(
path=request.path,
event=event,
envelope=envelope,
compressed=compressed,
)
)
response = Response(status=self.code)
response.headers.extend(self.headers)
return response(environ, start_response)
return CapturingServer()
sentry-python-2.64.0/tests/integrations/ 0000775 0000000 0000000 00000000000 15220673775 0020334 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0022433 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aiohttp/ 0000775 0000000 0000000 00000000000 15220673775 0022004 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aiohttp/__init__.py 0000664 0000000 0000000 00000000443 15220673775 0024116 0 ustar 00root root 0000000 0000000 import os
import sys
import pytest
pytest.importorskip("aiohttp")
# Load `aiohttp_helpers` into the module search path to test request source path names relative to module. See
# `test_request_source_with_module_in_search_path`
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
sentry-python-2.64.0/tests/integrations/aiohttp/aiohttp_helpers/ 0000775 0000000 0000000 00000000000 15220673775 0025176 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aiohttp/aiohttp_helpers/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0027275 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aiohttp/aiohttp_helpers/helpers.py 0000664 0000000 0000000 00000000112 15220673775 0027204 0 ustar 00root root 0000000 0000000 async def get_request_with_client(client, url):
await client.get(url)
sentry-python-2.64.0/tests/integrations/aiohttp/test_aiohttp.py 0000664 0000000 0000000 00000136344 15220673775 0025100 0 ustar 00root root 0000000 0000000 import asyncio
import datetime
import json
import os
from contextlib import suppress
from unittest import mock
import pytest
from aiohttp import web
from aiohttp.client import ServerDisconnectedError
from aiohttp.web_exceptions import (
HTTPBadRequest,
HTTPInternalServerError,
HTTPNetworkAuthenticationRequired,
HTTPNotFound,
HTTPUnavailableForLegalReasons,
)
from aiohttp.web_request import Request
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.aiohttp import (
AioHttpIntegration,
create_trace_config,
)
from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
from tests.conftest import ApproxDict
@pytest.mark.asyncio
async def test_basic(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
(event,) = events
assert (
event["transaction"]
== "tests.integrations.aiohttp.test_aiohttp.test_basic..hello"
)
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
request = event["request"]
host = request["headers"]["Host"]
assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"}
assert request["method"] == "GET"
assert request["query_string"] == ""
assert request.get("data") is None
assert request["url"] == "http://{host}/".format(host=host)
assert request["headers"] == {
"Accept": "*/*",
"Accept-Encoding": mock.ANY,
"Host": host,
"User-Agent": request["headers"]["User-Agent"],
"baggage": mock.ANY,
"sentry-trace": mock.ANY,
}
@pytest.mark.asyncio
async def test_post_body_not_read(sentry_init, aiohttp_client, capture_events):
from sentry_sdk.integrations.aiohttp import BODY_NOT_READ_MESSAGE
sentry_init(integrations=[AioHttpIntegration()])
body = {"some": "value"}
async def hello(request):
1 / 0
app = web.Application()
app.router.add_post("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.post("/", json=body)
assert resp.status == 500
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
request = event["request"]
assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"}
assert request["method"] == "POST"
assert request["data"] == BODY_NOT_READ_MESSAGE
@pytest.mark.asyncio
async def test_post_body_read(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
body = {"some": "value"}
async def hello(request):
await request.json()
1 / 0
app = web.Application()
app.router.add_post("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.post("/", json=body)
assert resp.status == 500
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
request = event["request"]
assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"}
assert request["method"] == "POST"
assert request["data"] == json.dumps(body)
@pytest.mark.asyncio
async def test_403_not_captured(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
raise web.HTTPForbidden()
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 403
assert not events
@pytest.mark.asyncio
async def test_cancelled_error_not_captured(
sentry_init, aiohttp_client, capture_events
):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
raise asyncio.CancelledError()
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
with suppress(ServerDisconnectedError):
# Intended `aiohttp` interaction: server will disconnect if it
# encounters `asyncio.CancelledError`
await client.get("/")
assert not events
@pytest.mark.asyncio
async def test_half_initialized(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()])
sentry_init()
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 200
assert events == []
@pytest.mark.asyncio
async def test_tracing(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()], traces_sample_rate=1.0)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 200
(event,) = events
assert event["type"] == "transaction"
assert (
event["transaction"]
== "tests.integrations.aiohttp.test_aiohttp.test_tracing..hello"
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
(
"/message",
"handler_name",
"tests.integrations.aiohttp.test_aiohttp.test_transaction_style..hello",
"component",
),
(
"/message",
"method_and_path_pattern",
"GET /{var}",
"route",
),
],
)
async def test_transaction_style(
sentry_init,
aiohttp_client,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(
integrations=[AioHttpIntegration(transaction_style=transaction_style)],
traces_sample_rate=1.0,
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/{var}", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get(url)
assert resp.status == 200
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
@pytest.mark.tests_internal_exceptions
@pytest.mark.asyncio
async def test_tracing_unparseable_url(sentry_init, aiohttp_client, capture_events):
sentry_init(integrations=[AioHttpIntegration()], traces_sample_rate=1.0)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
with mock.patch(
"sentry_sdk.integrations.aiohttp.parse_url", side_effect=ValueError
):
resp = await client.get("/")
assert resp.status == 200
(event,) = events
assert event["type"] == "transaction"
assert (
event["transaction"]
== "tests.integrations.aiohttp.test_aiohttp.test_tracing_unparseable_url..hello"
)
@pytest.mark.asyncio
async def test_traces_sampler_gets_request_object_in_sampling_context(
sentry_init,
aiohttp_client,
DictionaryContaining, # noqa: N803
ObjectDescribedBy, # noqa: N803
):
traces_sampler = mock.Mock()
sentry_init(
integrations=[AioHttpIntegration()],
traces_sampler=traces_sampler,
)
async def kangaroo_handler(request):
return web.Response(text="dogs are great")
app = web.Application()
app.router.add_get("/tricks/kangaroo", kangaroo_handler)
client = await aiohttp_client(app)
await client.get("/tricks/kangaroo")
traces_sampler.assert_any_call(
DictionaryContaining(
{
"aiohttp_request": ObjectDescribedBy(
type=Request, attrs={"method": "GET", "path": "/tricks/kangaroo"}
)
}
)
)
@pytest.mark.asyncio
async def test_has_trace_if_performance_enabled(
sentry_init, aiohttp_client, capture_events
):
sentry_init(integrations=[AioHttpIntegration()], traces_sample_rate=1.0)
async def hello(request):
capture_message("It's a good day to try dividing by 0")
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
msg_event, error_event, transaction_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
== msg_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.asyncio
async def test_has_trace_if_performance_disabled(
sentry_init, aiohttp_client, capture_events
):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
capture_message("It's a good day to try dividing by 0")
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
msg_event, error_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
error_event["contexts"]["trace"]["trace_id"]
== msg_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.asyncio
async def test_trace_from_headers_if_performance_enabled(
sentry_init, aiohttp_client, capture_events
):
sentry_init(integrations=[AioHttpIntegration()], traces_sample_rate=1.0)
async def hello(request):
capture_message("It's a good day to try dividing by 0")
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
# The aiohttp_client is instrumented so will generate the sentry-trace header and add request.
# Get the sentry-trace header from the request so we can later compare with transaction events.
client = await aiohttp_client(app)
with start_transaction():
# Headers are only added to the span if there is an active transaction
resp = await client.get("/")
sentry_trace_header = resp.request_info.headers.get("sentry-trace")
trace_id = sentry_trace_header.split("-")[0]
assert resp.status == 500
# Last item is the custom transaction event wrapping `client.get("/")`
msg_event, error_event, transaction_event, _ = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.asyncio
async def test_trace_from_headers_if_performance_disabled(
sentry_init, aiohttp_client, capture_events
):
sentry_init(integrations=[AioHttpIntegration()])
async def hello(request):
capture_message("It's a good day to try dividing by 0")
1 / 0
app = web.Application()
app.router.add_get("/", hello)
events = capture_events()
# The aiohttp_client is instrumented so will generate the sentry-trace header and add request.
# Get the sentry-trace header from the request so we can later compare with transaction events.
client = await aiohttp_client(app)
resp = await client.get("/")
sentry_trace_header = resp.request_info.headers.get("sentry-trace")
trace_id = sentry_trace_header.split("-")[0]
assert resp.status == 500
msg_event, error_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.asyncio
async def test_crumb_capture(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
):
def before_breadcrumb(crumb, hint):
crumb["data"]["extra"] = "foo"
return crumb
sentry_init(
integrations=[AioHttpIntegration()], before_breadcrumb=before_breadcrumb
)
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
with start_transaction():
events = capture_events()
client = await aiohttp_client(raw_server)
resp = await client.get("/")
assert resp.status == 200
capture_message("Testing!")
(event,) = events
crumb = event["breadcrumbs"]["values"][0]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == ApproxDict(
{
"url": "http://127.0.0.1:{}/".format(raw_server.port),
"http.fragment": "",
"http.method": "GET",
"http.query": "",
"http.response.status_code": 200,
"reason": "OK",
"extra": "foo",
}
)
@pytest.mark.parametrize(
"status_code,level",
[
(200, None),
(301, None),
(403, "warning"),
(405, "warning"),
(500, "error"),
],
)
@pytest.mark.asyncio
async def test_crumb_capture_client_error(
sentry_init,
aiohttp_raw_server,
aiohttp_client,
capture_events,
status_code,
level,
):
sentry_init(integrations=[AioHttpIntegration()])
async def handler(request):
return web.Response(status=status_code)
raw_server = await aiohttp_raw_server(handler)
with start_transaction():
events = capture_events()
client = await aiohttp_client(raw_server)
resp = await client.get("/")
assert resp.status == status_code
capture_message("Testing!")
(event,) = events
crumb = event["breadcrumbs"]["values"][0]
assert crumb["type"] == "http"
if level is None:
assert "level" not in crumb
else:
assert crumb["level"] == level
assert crumb["category"] == "httplib"
assert crumb["data"] == ApproxDict(
{
"url": "http://127.0.0.1:{}/".format(raw_server.port),
"http.fragment": "",
"http.method": "GET",
"http.query": "",
"http.response.status_code": status_code,
}
)
@pytest.mark.asyncio
async def test_outgoing_trace_headers(sentry_init, aiohttp_raw_server, aiohttp_client):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
)
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
# make trace_id difference between transactions
trace_id="0123456789012345678901234567890",
) as transaction:
client = await aiohttp_client(raw_server)
resp = await client.get("/")
request_span = transaction._span_recorder.spans[-1]
assert resp.request_info.headers[
"sentry-trace"
] == "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,
)
@pytest.mark.asyncio
async def test_outgoing_trace_headers_append_to_baggage(
sentry_init, aiohttp_raw_server, aiohttp_client
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
release="d08ebdb9309e1b004c6f52202de58a09c2268e42",
)
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
trace_id="0123456789012345678901234567890",
):
client = await aiohttp_client(raw_server)
resp = await client.get("/", headers={"bagGage": "custom=value"})
assert (
resp.request_info.headers["baggage"]
== "custom=value,sentry-trace_id=0123456789012345678901234567890,sentry-sample_rand=0.500000,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
)
@pytest.mark.asyncio
async def test_request_source_disabled(
sentry_init,
aiohttp_raw_server,
aiohttp_client,
capture_events,
):
sentry_options = {
"integrations": [AioHttpIntegration()],
"traces_sample_rate": 1.0,
"enable_http_request_source": False,
"http_request_source_threshold_ms": 0,
}
sentry_init(**sentry_options)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def hello(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", hello)
events = capture_events()
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
@pytest.mark.parametrize("enable_http_request_source", [None, True])
async def test_request_source_enabled(
sentry_init,
aiohttp_raw_server,
aiohttp_client,
capture_events,
enable_http_request_source,
):
sentry_options = {
"integrations": [AioHttpIntegration()],
"traces_sample_rate": 1.0,
"http_request_source_threshold_ms": 0,
}
if enable_http_request_source is not None:
sentry_options["enable_http_request_source"] = enable_http_request_source
sentry_init(**sentry_options)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def hello(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", hello)
events = capture_events()
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
@pytest.mark.asyncio
async def test_request_source(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
enable_http_request_source=True,
http_request_source_threshold_ms=0,
)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def handler_with_outgoing_request(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", handler_with_outgoing_request)
events = capture_events()
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/aiohttp/test_aiohttp.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request"
@pytest.mark.asyncio
async def test_request_source_with_module_in_search_path(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
):
"""
Test that request source is relative to the path of the module it ran in
"""
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
enable_http_request_source=True,
http_request_source_threshold_ms=0,
)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
from aiohttp_helpers.helpers import get_request_with_client
async def handler_with_outgoing_request(request):
span_client = await aiohttp_client(raw_server)
await get_request_with_client(span_client, "/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", handler_with_outgoing_request)
events = capture_events()
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert data.get(SPANDATA.CODE_NAMESPACE) == "aiohttp_helpers.helpers"
assert data.get(SPANDATA.CODE_FILEPATH) == "aiohttp_helpers/helpers.py"
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client"
@pytest.mark.asyncio
async def test_no_request_source_if_duration_too_short(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
enable_http_request_source=True,
http_request_source_threshold_ms=100,
)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def handler_with_outgoing_request(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", handler_with_outgoing_request)
events = capture_events()
def fake_create_trace_context(*args, **kwargs):
trace_context = create_trace_config()
async def overwrite_timestamps(session, trace_config_ctx, params):
span = trace_config_ctx.span
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
trace_context.on_request_end.insert(0, overwrite_timestamps)
return trace_context
with mock.patch(
"sentry_sdk.integrations.aiohttp.create_trace_config",
fake_create_trace_context,
):
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
async def test_request_source_if_duration_over_threshold(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_events
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
enable_http_request_source=True,
http_request_source_threshold_ms=100,
)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def handler_with_outgoing_request(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", handler_with_outgoing_request)
events = capture_events()
def fake_create_trace_context(*args, **kwargs):
trace_context = create_trace_config()
async def overwrite_timestamps(session, trace_config_ctx, params):
span = trace_config_ctx.span
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001)
trace_context.on_request_end.insert(0, overwrite_timestamps)
return trace_context
with mock.patch(
"sentry_sdk.integrations.aiohttp.create_trace_config",
fake_create_trace_context,
):
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("GET")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.aiohttp.test_aiohttp"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/aiohttp/test_aiohttp.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "handler_with_outgoing_request"
@pytest.mark.asyncio
async def test_span_origin(
sentry_init,
aiohttp_raw_server,
aiohttp_client,
capture_events,
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
)
# server for making span request
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def hello(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", hello)
events = capture_events()
client = await aiohttp_client(app)
await client.get("/")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.aiohttp"
assert event["spans"][0]["origin"] == "auto.http.aiohttp"
@pytest.mark.parametrize(
("integration_kwargs", "exception_to_raise", "should_capture"),
(
({}, None, False),
({}, HTTPBadRequest, False),
(
{},
HTTPUnavailableForLegalReasons(None),
False,
), # Highest 4xx status code (451)
({}, HTTPInternalServerError, True),
({}, HTTPNetworkAuthenticationRequired, True), # Highest 5xx status code (511)
({"failed_request_status_codes": set()}, HTTPInternalServerError, False),
(
{"failed_request_status_codes": set()},
HTTPNetworkAuthenticationRequired,
False,
),
({"failed_request_status_codes": {404, *range(500, 600)}}, HTTPNotFound, True),
(
{"failed_request_status_codes": {404, *range(500, 600)}},
HTTPInternalServerError,
True,
),
(
{"failed_request_status_codes": {404, *range(500, 600)}},
HTTPBadRequest,
False,
),
),
)
@pytest.mark.asyncio
async def test_failed_request_status_codes(
sentry_init,
aiohttp_client,
capture_events,
integration_kwargs,
exception_to_raise,
should_capture,
):
sentry_init(integrations=[AioHttpIntegration(**integration_kwargs)])
events = capture_events()
async def handle(_):
if exception_to_raise is not None:
raise exception_to_raise
else:
return web.Response(status=200)
app = web.Application()
app.router.add_get("/", handle)
client = await aiohttp_client(app)
resp = await client.get("/")
expected_status = (
200 if exception_to_raise is None else exception_to_raise.status_code
)
assert resp.status == expected_status
if should_capture:
(event,) = events
assert event["exception"]["values"][0]["type"] == exception_to_raise.__name__
else:
assert not events
@pytest.mark.asyncio
async def test_failed_request_status_codes_with_returned_status(
sentry_init, aiohttp_client, capture_events
):
"""
Returning a web.Response with a failed_request_status_code should not be reported to Sentry.
"""
sentry_init(integrations=[AioHttpIntegration(failed_request_status_codes={500})])
events = capture_events()
async def handle(_):
return web.Response(status=500)
app = web.Application()
app.router.add_get("/", handle)
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
assert not events
@pytest.mark.asyncio
async def test_failed_request_status_codes_non_http_exception(
sentry_init, aiohttp_client, capture_events
):
"""
If an exception, which is not an instance of HTTPException, is raised, it should be captured, even if
failed_request_status_codes is empty.
"""
sentry_init(integrations=[AioHttpIntegration(failed_request_status_codes=set())])
events = capture_events()
async def handle(_):
1 / 0
app = web.Application()
app.router.add_get("/", handle)
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
(event,) = events
assert event["exception"]["values"][0]["type"] == "ZeroDivisionError"
@pytest.mark.asyncio
@pytest.mark.parametrize("send_pii", [True, False])
async def test_tracing_span_streaming(
sentry_init, aiohttp_client, capture_items, send_pii
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_pii,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 200
sentry_sdk.flush()
# The aiohttp_client fixture is itself sentry-instrumented and emits the
# first http.client segment; the server-side http.server span is the other
# segment. Asserting the exact length confirms no other spans leak in.
assert len(items) == 2
server_span, client_span = [item.payload for item in items]
assert client_span["is_segment"] is True
assert client_span["attributes"]["sentry.op"] == "http.client"
assert client_span["name"].startswith("GET http://127.0.0.1:")
assert server_span["is_segment"] is True
assert (
server_span["name"]
== "tests.integrations.aiohttp.test_aiohttp.test_tracing_span_streaming..hello"
)
assert server_span["attributes"]["sentry.op"] == "http.server"
assert server_span["attributes"]["sentry.origin"] == "auto.http.aiohttp"
assert server_span["attributes"]["http.response.status_code"] == 200
assert server_span["attributes"]["sentry.span.source"] == "component"
assert server_span["status"] == "ok"
# No query string on the request, so the attribute should be omitted.
assert "url.query" not in server_span["attributes"]
# Request attributes derived directly from the aiohttp request.
assert server_span["attributes"]["http.request.method"] == "GET"
if send_pii:
assert "client.address" in server_span["attributes"]
assert "user.ip_address" in server_span["attributes"]
url_full = server_span["attributes"]["url.full"]
assert url_full.startswith("http://127.0.0.1:")
assert url_full.endswith("/")
url_path = server_span["attributes"]["url.path"]
assert url_path == "/"
else:
assert "url.full" not in server_span["attributes"]
assert "url.path" not in server_span["attributes"]
assert "url.query" not in server_span["attributes"]
assert "client.address" not in server_span["attributes"]
assert "user.ip_address" not in server_span["attributes"]
# aiohttp's test client always sends a Host header; we assert it propagates
# into the span attributes via _filter_headers.
assert "http.request.header.host" in server_span["attributes"]
@pytest.mark.asyncio
async def test_sensitive_header_scrubbing_span_streaming(
sentry_init, aiohttp_client, capture_items
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
resp = await client.get(
"/",
headers={
"Authorization": "Bearer secret-token",
"X-Custom-Header": "passthrough",
},
)
assert resp.status == 200
sentry_sdk.flush()
server_span, _client_segment = [item.payload for item in items]
# send_default_pii defaults to False, so _filter_headers substitutes
# sensitive headers with SENSITIVE_DATA_SUBSTITUTE ("[Filtered]"). The
# original token must not leak.
assert (
server_span["attributes"]["http.request.header.authorization"]
== SENSITIVE_DATA_SUBSTITUTE
)
# Non-sensitive headers pass through untouched.
assert (
server_span["attributes"]["http.request.header.x-custom-header"]
== "passthrough"
)
@pytest.mark.asyncio
async def test_sensitive_header_passthrough_with_pii_span_streaming(
sentry_init, aiohttp_client, capture_items
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
await client.get("/", headers={"Authorization": "Bearer secret-token"})
sentry_sdk.flush()
server_span, _client_segment = [item.payload for item in items]
# With send_default_pii=True, _filter_headers is a no-op and the original
# value reaches the span attribute.
assert (
server_span["attributes"]["http.request.header.authorization"]
== "Bearer secret-token"
)
# client.address and user.ip_address is captured under send_default_pii=True.
assert server_span["attributes"]["client.address"] == "127.0.0.1"
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
@pytest.mark.asyncio
@pytest.mark.parametrize("send_pii", [True, False])
async def test_url_query_attribute_span_streaming(
sentry_init, aiohttp_client, capture_items, send_pii
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_pii,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
resp = await client.get("/?foo=bar&baz=qux")
assert resp.status == 200
sentry_sdk.flush()
assert len(items) == 2
server_segment, client_segment = [item.payload for item in items]
if send_pii:
assert server_segment["attributes"]["url.query"] == "foo=bar&baz=qux"
else:
assert "url.query" not in server_segment["attributes"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"url,transaction_style,expected_name,expected_source",
[
(
"/message",
"handler_name",
"tests.integrations.aiohttp.test_aiohttp."
"test_transaction_style_span_streaming..hello",
"component",
),
(
"/message",
"method_and_path_pattern",
"GET /{var}",
"route",
),
],
)
async def test_transaction_style_span_streaming(
sentry_init,
aiohttp_client,
capture_items,
url,
transaction_style,
expected_name,
expected_source,
):
sentry_init(
integrations=[AioHttpIntegration(transaction_style=transaction_style)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/{var}", hello)
items = capture_items("span")
client = await aiohttp_client(app)
resp = await client.get(url)
assert resp.status == 200
sentry_sdk.flush()
assert len(items) == 2
server_segment, client_segment = [item.payload for item in items]
assert server_segment["name"] == expected_name
assert server_segment["is_segment"]
assert server_segment["attributes"]["sentry.span.source"] == expected_source
@pytest.mark.asyncio
async def test_server_error_span_streaming(sentry_init, aiohttp_client, capture_items):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
1 / 0
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("event", "span")
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 500
sentry_sdk.flush()
# 1 error event + 2 spans (server http.server, test client http.client segment)
assert len(items) == 3
error_event = items[0]
assert error_event.type == "event"
assert error_event.payload["exception"]["values"][0]["type"] == "ZeroDivisionError"
server_span, segment = [item.payload for item in items[1:]]
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.op"] == "http.client"
# The test client receives the 500 response that aiohttp's outer error
# handler synthesizes from the unhandled exception.
assert segment["attributes"]["http.response.status_code"] == 500
assert segment["status"] == "error"
# The integration's generic Exception path reraises without recording
# http.response.status_code on the server span. StreamedSpan.__exit__
# observes the propagating exception and sets status to "error".
assert server_span["attributes"]["sentry.op"] == "http.server"
assert "http.response.status_code" not in server_span["attributes"]
assert server_span["status"] == "error"
@pytest.mark.asyncio
async def test_http_exception_span_streaming(
sentry_init, aiohttp_client, capture_items
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
raise web.HTTPForbidden()
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
resp = await client.get("/")
assert resp.status == 403
sentry_sdk.flush()
assert len(items) == 2
server_span, segment = [item.payload for item in items]
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.op"] == "http.client"
assert segment["attributes"]["http.response.status_code"] == 403
assert segment["status"] == "error"
assert server_span["attributes"]["sentry.op"] == "http.server"
assert server_span["attributes"]["http.response.status_code"] == 403
assert server_span["status"] == "error"
@pytest.mark.asyncio
async def test_http_exception_ok_status_not_overridden_span_streaming(
sentry_init, aiohttp_client, capture_items
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
raise web.HTTPFound("https://example.com")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app, server_kwargs={"skip_url_asserts": True})
resp = await client.get("/", allow_redirects=False)
assert resp.status == 302
sentry_sdk.flush()
assert len(items) == 2
server_span, segment = [item.payload for item in items]
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.op"] == "http.client"
assert segment["attributes"]["http.response.status_code"] == 302
assert segment["status"] == "ok"
assert server_span["attributes"]["sentry.op"] == "http.server"
assert server_span["attributes"]["http.response.status_code"] == 302
assert server_span["status"] == "ok"
@pytest.mark.asyncio
@pytest.mark.parametrize("send_pii", [True, False])
async def test_outgoing_client_span_span_streaming(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_items, send_pii
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_pii,
_experiments={"trace_lifecycle": "stream"},
)
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
async def hello(request):
span_client = await aiohttp_client(raw_server)
await span_client.get("/?foo=bar")
return web.Response(text="hello")
app = web.Application()
app.router.add_get(r"/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
await client.get("/")
sentry_sdk.flush()
# 3 spans, finished inner-first:
# #0 inner http.client (server -> raw_server)
# #1 server http.server
# #2 outer http.client segment (test client -> server)
assert len(items) == 3
inner_client_span = items[0].payload
server_span = items[1].payload
segment = items[2].payload
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.op"] == "http.client"
assert server_span["attributes"]["sentry.op"] == "http.server"
assert inner_client_span["is_segment"] is False
assert inner_client_span["name"].startswith("GET ")
assert inner_client_span["attributes"]["sentry.op"] == "http.client"
assert inner_client_span["attributes"]["sentry.origin"] == "auto.http.aiohttp"
assert inner_client_span["attributes"]["http.request.method"] == "GET"
assert inner_client_span["attributes"]["http.response.status_code"] == 200
assert inner_client_span["status"] == "ok"
if send_pii:
assert inner_client_span["attributes"]["url.query"] == "foo=bar"
url_full = inner_client_span["attributes"]["url.full"]
# parse_url() splits the URL — url.full is the base URL only, with the
# query string captured separately on url.query.
assert url_full.startswith("http://127.0.0.1:")
assert url_full.endswith("/")
assert inner_client_span["attributes"]["url.path"] == "/"
@pytest.mark.asyncio
async def test_outgoing_trace_headers_span_streaming(
sentry_init, aiohttp_raw_server, aiohttp_client, capture_items
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
async def handler(request):
return web.Response(text="OK")
raw_server = await aiohttp_raw_server(handler)
items = capture_items("span")
client = await aiohttp_client(raw_server)
resp = await client.get("/")
sentry_sdk.flush()
# raw_server bypasses Application._handle, so only the test client's
# outgoing http.client segment is emitted.
assert len(items) == 1
client_span = items[0].payload
assert client_span["is_segment"] is True
assert client_span["attributes"]["sentry.op"] == "http.client"
assert client_span["name"].startswith("GET http://127.0.0.1:")
assert resp.request_info.headers[
"sentry-trace"
] == "{trace_id}-{span_id}-{sampled}".format(
trace_id=client_span["trace_id"],
span_id=client_span["span_id"],
sampled=1,
)
@pytest.mark.asyncio
@pytest.mark.parametrize("send_default_pii", [True, False])
async def test_user_ip_address_on_all_spans(
sentry_init, aiohttp_client, capture_items, send_default_pii
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream"},
)
async def hello(request):
with sentry_sdk.traces.start_span(name="child-span"):
pass
return web.Response(text="hello")
app = web.Application()
app.router.add_get("/", hello)
items = capture_items("span")
client = await aiohttp_client(app)
await client.get("/")
sentry_sdk.flush()
child_span, server_span, client_span = [item.payload for item in items]
if send_default_pii:
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
else:
assert "user.ip_address" not in server_span["attributes"]
assert "user.ip_address" not in child_span["attributes"]
sentry-python-2.64.0/tests/integrations/aiomysql/ 0000775 0000000 0000000 00000000000 15220673775 0022172 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aiomysql/__init__.py 0000664 0000000 0000000 00000000125 15220673775 0024301 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("aiomysql")
pytest.importorskip("pytest_asyncio")
sentry-python-2.64.0/tests/integrations/aiomysql/test_aiomysql.py 0000664 0000000 0000000 00000105420 15220673775 0025443 0 ustar 00root root 0000000 0000000 """
Tests need pytest-asyncio installed.
Tests need a local MySQL instance running. This can be done using:
```sh
docker run --rm --name some-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test -d -p 3306:3306 mysql:8.0
```
The tests use the following credentials to establish a database connection.
"""
import datetime
import os
import warnings
from contextlib import contextmanager
from unittest import mock
import aiomysql
import pytest
import pytest_asyncio
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.aiomysql import AioMySQLIntegration
from sentry_sdk.tracing_utils import record_sql_queries
from tests.conftest import ApproxDict
MYSQL_HOST = os.getenv("SENTRY_PYTHON_TEST_MYSQL_HOST", "localhost")
MYSQL_PORT = int(os.getenv("SENTRY_PYTHON_TEST_MYSQL_PORT", "3306"))
MYSQL_USER = os.getenv("SENTRY_PYTHON_TEST_MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_MYSQL_PASSWORD", "root")
MYSQL_DB_BASE = os.getenv("SENTRY_PYTHON_TEST_MYSQL_DB", "test")
def _get_db_name():
pid = os.getpid()
return f"{MYSQL_DB_BASE}_{pid}"
MYSQL_DB = _get_db_name()
CRUMBS_CONNECT = {
"category": "query",
"data": ApproxDict(
{
"db.name": MYSQL_DB,
"db.system": "mysql",
"db.user": MYSQL_USER,
"server.address": MYSQL_HOST,
"server.port": MYSQL_PORT,
}
),
"message": "connect",
"type": "default",
}
@pytest_asyncio.fixture(autouse=True)
async def _clean_mysql():
conn = await aiomysql.connect(
host=MYSQL_HOST,
port=MYSQL_PORT,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
autocommit=True,
)
try:
async with conn.cursor() as cur:
# Suppress MySQL warnings about unknown tables / existing databases
with warnings.catch_warnings():
warnings.simplefilter("ignore", Warning)
await cur.execute(f"CREATE DATABASE IF NOT EXISTS `{MYSQL_DB}`")
await cur.execute(f"USE `{MYSQL_DB}`")
await cur.execute("DROP TABLE IF EXISTS users")
await cur.execute(
"""
CREATE TABLE users(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
password VARCHAR(255),
dob DATE
)
"""
)
finally:
conn.close()
def _connect_args():
return {
"host": MYSQL_HOST,
"port": MYSQL_PORT,
"user": MYSQL_USER,
"password": MYSQL_PASSWORD,
"db": MYSQL_DB,
"autocommit": True,
}
@pytest.mark.asyncio
async def test_connect(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [CRUMBS_CONNECT]
@pytest.mark.asyncio
async def test_execute(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')",
)
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
)
await cur.execute("SELECT * FROM users WHERE name = %s", ("Bob",))
row = await cur.fetchone()
assert row[1] == "Bob"
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {},
"message": "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT * FROM users WHERE name = %s",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_execute_many(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.executemany(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {"db.executemany": True},
"message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_execute_many_non_insert(sentry_init, capture_events) -> None:
"""Test executemany with non-INSERT queries (falls back to row-by-row)."""
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
# Pre-populate users table
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Alice", "pw1", datetime.date(1990, 1, 1)),
)
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Bob", "pw2", datetime.date(1991, 2, 2)),
)
# Non-INSERT executemany — uses row-by-row fallback internally
await cur.executemany(
"UPDATE users SET password = %s WHERE name = %s",
[
("new_pw_1", "Alice"),
("new_pw_2", "Bob"),
],
)
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
# Should have: connect + INSERT*2 + single executemany span (no double-recording)
crumbs = event["breadcrumbs"]["values"]
query_crumbs = [c for c in crumbs if c["category"] == "query"]
executemany_crumbs = [
c for c in query_crumbs if c.get("data", {}).get("db.executemany")
]
# Only ONE executemany breadcrumb — no duplicates from internal execute calls
assert len(executemany_crumbs) == 1
assert "UPDATE users SET password = %s" in executemany_crumbs[0]["message"]
@pytest.mark.asyncio
async def test_record_params(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration(record_params=True)],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
)
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {
"db.params": ["Bob", "secret_pw", "datetime.date(1984, 3, 1)"],
"db.paramstyle": "format",
},
"message": "INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_cursor_context_manager(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.executemany(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
await cur.execute(
"SELECT * FROM users WHERE dob > %s",
(datetime.date(1970, 1, 1),),
)
rows = await cur.fetchall()
assert len(rows) == 2
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
crumbs = event["breadcrumbs"]["values"]
assert crumbs[0] == CRUMBS_CONNECT
assert crumbs[1]["category"] == "query"
assert "INSERT" in crumbs[1]["message"]
assert crumbs[2]["category"] == "query"
assert "SELECT" in crumbs[2]["message"]
@pytest.mark.asyncio
async def test_cursor_async_iteration(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Charlie", "pw3", datetime.date(1995, 5, 15)),
)
await cur.execute("SELECT * FROM users WHERE name = %s", ("Charlie",))
async for row in cur:
assert row[1] == "Charlie"
conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
crumbs = event["breadcrumbs"]["values"]
assert crumbs[0] == CRUMBS_CONNECT
assert len([c for c in crumbs if c["category"] == "query"]) >= 2
@pytest.mark.asyncio
async def test_connection_pool(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AioMySQLIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
pool_size = 2
pool = await aiomysql.create_pool(
host=MYSQL_HOST,
port=MYSQL_PORT,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
db=MYSQL_DB,
autocommit=True,
minsize=pool_size,
maxsize=pool_size,
)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES (%s, %s, %s)",
("Dave", "pw4", datetime.date(1988, 7, 20)),
)
await cur.execute("SELECT * FROM users WHERE name = %s", ("Dave",))
row = await cur.fetchone()
assert row is not None
assert row[1] == "Dave"
pool.close()
await pool.wait_closed()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
crumbs = event["breadcrumbs"]["values"]
# Verify queries were captured
query_crumbs = [c for c in crumbs if c["category"] == "query"]
assert len(query_crumbs) >= 2 # INSERT + SELECT
# Verify connect spans were created for pool connections
connect_crumbs = [c for c in crumbs if c.get("message") == "connect"]
assert len(connect_crumbs) >= pool_size # One connect span per pooled connection
for crumb in connect_crumbs:
assert crumb["data"]["db.system"] == "mysql"
assert crumb["data"]["server.address"] == MYSQL_HOST
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_disabled(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_options = {
"integrations": [AioMySQLIntegration()],
"traces_sample_rate": 1.0,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
"_experiments": {
"trace_lifecycle": "stream" if span_streaming else "static",
},
}
sentry_init(**sentry_options)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["is_segment"] is True
assert connect_span["name"] == "connect"
assert insert_span["name"].startswith("INSERT INTO")
data = insert_span.get("attributes", {})
assert "code.line.number" not in data
assert "code.namespace" not in data
assert "code.file.path" not in data
assert "code.function" not in data
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
@pytest.mark.parametrize("enable_db_query_source", [None, True])
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_enabled(
sentry_init, capture_events, capture_items, enable_db_query_source, span_streaming
):
sentry_options = {
"integrations": [AioMySQLIntegration()],
"traces_sample_rate": 1.0,
"db_query_source_threshold_ms": 0,
"_experiments": {
"trace_lifecycle": "stream" if span_streaming else "static",
},
}
if enable_db_query_source is not None:
sentry_options["enable_db_query_source"] = enable_db_query_source
sentry_init(**sentry_options)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["is_segment"] is True
assert connect_span["name"] == "connect"
assert insert_span["name"].startswith("INSERT INTO")
data = insert_span.get("attributes", {})
assert "code.line.number" in data
assert "code.namespace" in data
assert "code.file.path" in data
assert "code.function" in data
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source(sentry_init, capture_events, capture_items, span_streaming):
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["is_segment"] is True
assert connect_span["name"] == "connect"
assert insert_span["name"].startswith("INSERT INTO")
data = insert_span.get("attributes", {})
assert "code.line.number" in data
assert "code.namespace" in data
assert "code.file.path" in data
assert "code.function" in data
assert type(data.get("code.line.number")) == int
assert data.get("code.line.number") > 0
assert data.get("code.namespace") == "tests.integrations.aiomysql.test_aiomysql"
assert data.get("code.file.path").endswith(
"tests/integrations/aiomysql/test_aiomysql.py"
)
is_relative_path = data.get("code.file.path")[0] != os.sep
assert is_relative_path
assert data.get("code.function") == "test_query_source"
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.aiomysql.test_aiomysql"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/aiomysql/test_aiomysql.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_no_query_source_if_duration_too_short(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
@contextmanager
def fake_record_sql_queries_streaming(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span._start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span._end_timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
yield span
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
with mock.patch(
"sentry_sdk.integrations.aiomysql.record_sql_queries",
fake_record_sql_queries_streaming,
):
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
assert "code.line.number" not in data
assert "code.namespace" not in data
assert "code.file.path" not in data
assert "code.function" not in data
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn = await aiomysql.connect(**_connect_args())
@contextmanager
def fake_record_sql_queries(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
yield span
async with conn.cursor() as cur:
with mock.patch(
"sentry_sdk.integrations.aiomysql.record_sql_queries",
fake_record_sql_queries,
):
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_if_duration_over_threshold(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
@contextmanager
def fake_record_sql_queries_streaming(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
span._start_timestamp = datetime.datetime(
2024, 1, 1, microsecond=0, tzinfo=datetime.timezone.utc
)
yield span
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
with mock.patch(
"sentry_sdk.integrations.aiomysql.record_sql_queries",
fake_record_sql_queries_streaming,
):
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
assert "code.line.number" in data
assert "code.namespace" in data
assert "code.file.path" in data
assert "code.function" in data
assert type(data.get("code.line.number")) == int
assert data.get("code.line.number") > 0
assert data.get("code.namespace") == "tests.integrations.aiomysql.test_aiomysql"
assert data.get("code.file.path").endswith(
"tests/integrations/aiomysql/test_aiomysql.py"
)
is_relative_path = data.get("code.file.path")[0] != os.sep
assert is_relative_path
assert (
data.get("code.function") == "test_query_source_if_duration_over_threshold"
)
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn = await aiomysql.connect(**_connect_args())
@contextmanager
def fake_record_sql_queries(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001)
yield span
async with conn.cursor() as cur:
with mock.patch(
"sentry_sdk.integrations.aiomysql.record_sql_queries",
fake_record_sql_queries,
):
await cur.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.aiomysql.test_aiomysql"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/aiomysql/test_aiomysql.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert (
data.get(SPANDATA.CODE_FUNCTION)
== "test_query_source_if_duration_over_threshold"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_span_origin(sentry_init, capture_events, capture_items, span_streaming):
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
await cur.execute("SELECT 2")
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
non_segment_spans = [s for s in spans if not s.get("is_segment")]
segment_spans = [s for s in spans if s.get("is_segment")]
assert len(segment_spans) == 1
assert segment_spans[0]["attributes"]["sentry.origin"] == "manual"
for span in non_segment_spans:
assert span["attributes"]["sentry.origin"] == "auto.db.aiomysql"
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
await cur.execute("SELECT 2")
conn.close()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.aiomysql"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_multiline_query_description_normalized(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT
id,
name
FROM
users
WHERE
name = 'Alice'
"""
)
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
select_spans = [
s
for s in spans
if not s.get("is_segment") and "SELECT" in s.get("name", "")
]
assert len(select_spans) == 1
assert (
select_spans[0]["name"] == "SELECT id, name FROM users WHERE name = 'Alice'"
)
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT
id,
name
FROM
users
WHERE
name = 'Alice'
"""
)
conn.close()
(event,) = events
spans = [
s
for s in event["spans"]
if s["op"] == "db" and "SELECT" in s.get("description", "")
]
assert len(spans) == 1
assert (
spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'"
)
@pytest.mark.asyncio
async def test_before_send_transaction_sees_normalized_description(
sentry_init, capture_events
):
def before_send_transaction(event, hint):
for span in event.get("spans", []):
desc = span.get("description", "")
if "SELECT id, name FROM users" in desc:
span["description"] = "filtered"
return event
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
before_send_transaction=before_send_transaction,
)
events = capture_events()
with start_transaction(name="test_transaction"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute(
"""
SELECT
id,
name
FROM
users
"""
)
conn.close()
(event,) = events
spans = [
s
for s in event["spans"]
if s["op"] == "db" and "filtered" in s.get("description", "")
]
assert len(spans) == 1
assert spans[0]["description"] == "filtered"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_db_data_on_spans(
sentry_init, capture_events, capture_items, span_streaming
):
"""Test that database connection data is properly set on spans."""
sentry_init(
integrations=[AioMySQLIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
query_span = [
s
for s in spans
if not s.get("is_segment") and "SELECT" in s.get("name", "")
][0]
data = query_span.get("attributes", {})
assert data.get(SPANDATA.DB_SYSTEM_NAME) == "mysql"
assert data.get(SPANDATA.SERVER_ADDRESS) == MYSQL_HOST
assert data.get(SPANDATA.SERVER_PORT) == MYSQL_PORT
assert data.get(SPANDATA.DB_NAMESPACE) == MYSQL_DB
assert data.get(SPANDATA.DB_USER) == MYSQL_USER
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn = await aiomysql.connect(**_connect_args())
async with conn.cursor() as cur:
await cur.execute("SELECT 1")
conn.close()
(event,) = events
db_spans = [s for s in event["spans"] if s["op"] == "db"]
assert len(db_spans) > 0
query_span = [s for s in db_spans if "SELECT" in s.get("description", "")][0]
assert query_span["data"].get(SPANDATA.DB_SYSTEM) == "mysql"
assert query_span["data"].get(SPANDATA.SERVER_ADDRESS) == MYSQL_HOST
assert query_span["data"].get(SPANDATA.SERVER_PORT) == MYSQL_PORT
assert query_span["data"].get(SPANDATA.DB_NAME) == MYSQL_DB
assert query_span["data"].get(SPANDATA.DB_USER) == MYSQL_USER
sentry-python-2.64.0/tests/integrations/anthropic/ 0000775 0000000 0000000 00000000000 15220673775 0022323 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/anthropic/__init__.py 0000664 0000000 0000000 00000000060 15220673775 0024430 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("anthropic")
sentry-python-2.64.0/tests/integrations/anthropic/test_anthropic.py 0000664 0000000 0000000 00000725405 15220673775 0025740 0 ustar 00root root 0000000 0000000 import json
from unittest import mock
import pytest
import sentry_sdk
try:
from unittest.mock import AsyncMock
except ImportError:
class AsyncMock(mock.MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
from anthropic import Anthropic, AnthropicError, AsyncAnthropic
from anthropic.types import MessageDeltaUsage, TextDelta, Usage
from anthropic.types.content_block_delta_event import ContentBlockDeltaEvent
from anthropic.types.content_block_start_event import ContentBlockStartEvent
from anthropic.types.content_block_stop_event import ContentBlockStopEvent
from anthropic.types.message import Message
from anthropic.types.message_delta_event import MessageDeltaEvent
from anthropic.types.message_start_event import MessageStartEvent
try:
from anthropic import APIStatusError
from anthropic.types import ErrorResponse, OverloadedError
except ImportError:
ErrorResponse = None
OverloadedError = None
APIStatusError = None
try:
from anthropic.types import InputJSONDelta
except ImportError:
try:
from anthropic.types import InputJsonDelta as InputJSONDelta
except ImportError:
pass
try:
from anthropic.lib.streaming import TextEvent
except ImportError:
TextEvent = None
try:
# 0.27+
from anthropic.types.raw_message_delta_event import Delta
from anthropic.types.tool_use_block import ToolUseBlock
except ImportError:
# pre 0.27
from anthropic.types.message_delta_event import Delta
try:
from anthropic.types.text_block import TextBlock
except ImportError:
from anthropic.types.content_block import ContentBlock as TextBlock
from sentry_sdk import start_span, start_transaction
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
from sentry_sdk.ai.utils import transform_content_part, transform_message_content
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations.anthropic import (
AnthropicIntegration,
_collect_ai_data,
_RecordedUsage,
_set_output_data,
_transform_anthropic_content_block,
)
from sentry_sdk.utils import package_version
ANTHROPIC_VERSION = package_version("anthropic")
EXAMPLE_MESSAGE = Message(
id="msg_01XFDUDYJgAACzvnptvVoYEL",
model="model",
role="assistant",
content=[TextBlock(type="text", text="Hi, I'm Claude.")],
type="message",
stop_reason="end_turn",
usage=Usage(input_tokens=10, output_tokens=20),
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_nonstreaming_create_message(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"end_turn"
]
else:
events = capture_events()
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_nonstreaming_create_message_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
response = await client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
response = await client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_streaming_create_message(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"max_tokens"
]
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_streaming_create_message_close(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
messages = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in range(4):
next(messages)
messages.close()
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
messages = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in range(4):
next(messages)
messages.close()
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 41),
reason="Error classes moved in https://github.com/anthropics/anthropic-sdk-python/commit/4e0b15e22fe40e9aa513459564f641bf97c90954.",
)
def test_streaming_create_message_api_error(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ErrorResponse(
type="error",
error=OverloadedError(
message="Overloaded", type="overloaded_error"
),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "error"
else:
events = capture_events()
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "internal_error"
assert span["tags"]["status"] == "internal_error"
assert event["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_stream_messages(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"max_tokens"
]
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_stream_messages_close(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for _ in range(4):
next(stream)
# New versions add TextEvent, so consume one more event.
if TextEvent is not None and isinstance(next(stream), TextEvent):
next(stream)
stream.close()
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for _ in range(4):
next(stream)
# New versions add TextEvent, so consume one more event.
if TextEvent is not None and isinstance(next(stream), TextEvent):
next(stream)
stream.close()
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 41),
reason="Error classes moved in https://github.com/anthropics/anthropic-sdk-python/commit/4e0b15e22fe40e9aa513459564f641bf97c90954.",
)
def test_stream_messages_api_error(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ErrorResponse(
type="error",
error=OverloadedError(
message="Overloaded", type="overloaded_error"
),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "error"
else:
events = capture_events()
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "internal_error"
assert span["tags"]["status"] == "internal_error"
assert event["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_streaming_create_message_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
),
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
default_integrations=False,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"max_tokens"
]
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["max_tokens"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
async def test_streaming_create_message_async_close(
sentry_init,
capture_events,
capture_items,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
messages = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in range(4):
await messages.__anext__()
await messages.close()
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
messages = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in range(4):
await messages.__anext__()
await messages.close()
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 41),
reason="Error classes moved in https://github.com/anthropics/anthropic-sdk-python/commit/4e0b15e22fe40e9aa513459564f641bf97c90954.",
)
@pytest.mark.asyncio
async def test_streaming_create_message_async_api_error(
sentry_init,
capture_events,
capture_items,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ErrorResponse(
type="error",
error=OverloadedError(
message="Overloaded", type="overloaded_error"
),
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "error"
else:
events = capture_events()
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "internal_error"
assert span["tags"]["status"] == "internal_error"
assert event["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_stream_message_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
),
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert json.loads(span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
{
"role": "user",
"content": "Message demonstrating the absence of truncation.",
},
{
"role": "user",
"content": "Hello, Claude",
},
]
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 41),
reason="Error classes moved in https://github.com/anthropics/anthropic-sdk-python/commit/4e0b15e22fe40e9aa513459564f641bf97c90954.",
)
@pytest.mark.asyncio
async def test_stream_messages_async_api_error(
sentry_init,
capture_events,
capture_items,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ErrorResponse(
type="error",
error=OverloadedError(
message="Overloaded", type="overloaded_error"
),
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "error"
else:
events = capture_events()
with pytest.raises(APIStatusError), mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
assert span["status"] == "internal_error"
assert span["tags"]["status"] == "internal_error"
assert event["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
async def test_stream_messages_async_close(
sentry_init,
capture_events,
capture_items,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="max_tokens"),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for _ in range(4):
await stream.__anext__()
# New versions add TextEvent, so consume one more event.
if TextEvent is not None and isinstance(
await stream.__anext__(), TextEvent
):
await stream.__anext__()
await stream.close()
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
span = next(
span for span in spans if span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
)
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_ID]
== "msg_01XFDUDYJgAACzvnptvVoYEL"
)
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for _ in range(4):
await stream.__anext__()
# New versions add TextEvent, so consume one more event.
if TextEvent is not None and isinstance(
await stream.__anext__(), TextEvent
):
await stream.__anext__()
await stream.close()
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
span = next(span for span in event["spans"] if span["op"] == OP.GEN_AI_CHAT)
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "Hello, Claude"}]'
)
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi!"
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_ID] == "msg_01XFDUDYJgAACzvnptvVoYEL"
)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.",
)
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_streaming_create_message_with_input_json_delta(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=Message(
id="msg_0",
content=[],
model="claude-3-5-sonnet-20240620",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=366, output_tokens=10),
),
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=ToolUseBlock(
id="toolu_0", input={}, name="get_weather", type="tool_use"
),
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='{"location": "', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="S", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="an ", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="Francisco, C", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json='A"}', type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="tool_use", stop_sequence=None),
usage=MessageDeltaUsage(output_tokens=41),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.",
)
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_stream_messages_with_input_json_delta(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=Message(
id="msg_0",
content=[],
model="claude-3-5-sonnet-20240620",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=366, output_tokens=10),
),
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=ToolUseBlock(
id="toolu_0", input={}, name="get_weather", type="tool_use"
),
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='{"location": "', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="S", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="an ", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="Francisco, C", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json='A"}', type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="tool_use", stop_sequence=None),
usage=MessageDeltaUsage(output_tokens=41),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.",
)
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_streaming_create_message_with_input_json_delta_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=Message(
id="msg_0",
content=[],
model="claude-3-5-sonnet-20240620",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=366, output_tokens=10),
),
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=ToolUseBlock(
id="toolu_0", input={}, name="get_weather", type="tool_use"
),
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='{"location": "', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="S", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="an ", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="Francisco, C", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='A"}', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="tool_use", stop_sequence=None),
usage=MessageDeltaUsage(output_tokens=41),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta, which was introduced in >=0.27.0 along with a new message delta type for tool calling.",
)
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_stream_message_with_input_json_delta_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=Message(
id="msg_0",
content=[],
model="claude-3-5-sonnet-20240620",
role="assistant",
stop_reason=None,
stop_sequence=None,
type="message",
usage=Usage(input_tokens=366, output_tokens=10),
),
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=ToolUseBlock(
id="toolu_0", input={}, name="get_weather", type="tool_use"
),
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='{"location": "', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="S", type="input_json_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="an ", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json="Francisco, C", type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=InputJSONDelta(
partial_json='A"}', type="input_json_delta"
),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(stop_reason="tool_use", stop_sequence=None),
usage=MessageDeltaUsage(output_tokens=41),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
) as stream:
async for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert (
span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
== '[{"role": "user", "content": "What is the weather like in San Francisco?"}]'
)
assert (
span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
== '{"location": "San Francisco, CA"}'
)
else:
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 366
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 41
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 407
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_exception_message_create(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
side_effect=AnthropicError("API rate limit reached")
)
if span_streaming:
items = capture_items("event")
with pytest.raises(AnthropicError):
client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event,) = (item.payload for item in items if item.type == "event")
assert event["level"] == "error"
elif stream_gen_ai_spans:
items = capture_items("event", "transaction")
with pytest.raises(AnthropicError):
client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event,) = (item.payload for item in items if item.type == "event")
assert event["level"] == "error"
(transaction,) = (item.payload for item in items if item.type == "transaction")
assert transaction["contexts"]["trace"]["status"] == "internal_error"
else:
events = capture_events()
with pytest.raises(AnthropicError):
client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event, transaction) = events
assert event["level"] == "error"
assert transaction["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_span_status_error(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("event", "span")
with start_transaction(name="anthropic"):
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
side_effect=AnthropicError("API rate limit reached")
)
with pytest.raises(AnthropicError):
client.messages.create(
model="some-model",
messages=[
{"role": "system", "content": "I'm throwing an exception"}
],
max_tokens=1024,
)
(error,) = (item.payload for item in items if item.type == "event")
assert error["level"] == "error"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["status"] == "error"
assert spans[0]["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
else:
events = capture_events()
with start_transaction(name="anthropic"):
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
side_effect=AnthropicError("API rate limit reached")
)
with pytest.raises(AnthropicError):
client.messages.create(
model="some-model",
messages=[
{"role": "system", "content": "I'm throwing an exception"}
],
max_tokens=1024,
)
(error, transaction) = events
assert error["level"] == "error"
assert transaction["spans"][0]["status"] == "internal_error"
assert transaction["spans"][0]["tags"]["status"] == "internal_error"
assert transaction["spans"][0]["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert transaction["spans"][0]["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
async def test_span_status_error_async(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("event", "span")
with start_transaction(name="anthropic"):
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(
side_effect=AnthropicError("API rate limit reached")
)
with pytest.raises(AnthropicError):
await client.messages.create(
model="some-model",
messages=[
{"role": "system", "content": "I'm throwing an exception"}
],
max_tokens=1024,
)
(error,) = (item.payload for item in items if item.type == "event")
assert error["level"] == "error"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["status"] == "error"
assert spans[0]["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
else:
events = capture_events()
with start_transaction(name="anthropic"):
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(
side_effect=AnthropicError("API rate limit reached")
)
with pytest.raises(AnthropicError):
await client.messages.create(
model="some-model",
messages=[
{"role": "system", "content": "I'm throwing an exception"}
],
max_tokens=1024,
)
(error, transaction) = events
assert error["level"] == "error"
assert transaction["spans"][0]["status"] == "internal_error"
assert transaction["spans"][0]["tags"]["status"] == "internal_error"
assert transaction["spans"][0]["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert transaction["spans"][0]["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
async def test_exception_message_create_async(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(
side_effect=AnthropicError("API rate limit reached")
)
if span_streaming:
items = capture_items("event")
with pytest.raises(AnthropicError):
await client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event,) = (item.payload for item in items if item.type == "event")
assert event["level"] == "error"
elif stream_gen_ai_spans:
items = capture_items("event", "transaction")
with pytest.raises(AnthropicError):
await client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event,) = (item.payload for item in items if item.type == "event")
assert event["level"] == "error"
(transaction,) = (item.payload for item in items if item.type == "transaction")
assert transaction["contexts"]["trace"]["status"] == "internal_error"
else:
events = capture_events()
with pytest.raises(AnthropicError):
await client.messages.create(
model="some-model",
messages=[{"role": "system", "content": "I'm throwing an exception"}],
max_tokens=1024,
)
(event, transaction) = events
assert event["level"] == "error"
assert transaction["contexts"]["trace"]["status"] == "internal_error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_span_origin(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["contexts"]["trace"]["origin"] == "manual"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["attributes"]["sentry.origin"] == "auto.ai.anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.anthropic"
assert event["spans"][0]["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert event["spans"][0]["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
async def test_span_origin_async(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
await client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["contexts"]["trace"]["origin"] == "manual"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[0]["attributes"]["sentry.origin"] == "auto.ai.anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert spans[0]["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
else:
events = capture_events()
with start_transaction(name="anthropic"):
await client.messages.create(
max_tokens=1024, messages=messages, model="model"
)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.anthropic"
assert event["spans"][0]["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert event["spans"][0]["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta.",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_collect_ai_data_with_input_json_delta(span_streaming):
event = ContentBlockDeltaEvent(
delta=InputJSONDelta(partial_json="test", type="input_json_delta"),
index=0,
type="content_block_delta",
)
model = None
usage = _RecordedUsage()
usage.output_tokens = 20
usage.input_tokens = 10
content_blocks = []
model, new_usage, new_content_blocks, response_id, finish_reason = _collect_ai_data(
event, model, usage, content_blocks
)
assert model is None
assert new_usage.input_tokens == usage.input_tokens
assert new_usage.output_tokens == usage.output_tokens
assert new_content_blocks == ["test"]
assert response_id is None
assert finish_reason is None
@pytest.mark.skipif(
ANTHROPIC_VERSION < (0, 27),
reason="Versions <0.27.0 do not include InputJSONDelta.",
)
def test_set_output_data_with_input_json_delta(sentry_init):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
)
with start_transaction(name="test"):
span = start_span()
integration = AnthropicIntegration()
json_deltas = ["{'test': 'data',", "'more': 'json'}"]
_set_output_data(
span,
integration,
model="",
input_tokens=10,
output_tokens=20,
cache_read_input_tokens=0,
cache_write_input_tokens=0,
content_blocks=[{"text": "".join(json_deltas), "type": "text"}],
)
assert (
span._data.get(SPANDATA.GEN_AI_RESPONSE_TEXT)
== "{'test': 'data','more': 'json'}"
)
assert span._data.get(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS) == 10
assert span._data.get(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS) == 20
assert span._data.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30
# Test messages with mixed roles including "ai" that should be mapped to "assistant"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"test_message,expected_role",
[
({"role": "system", "content": "You are helpful."}, "system"),
({"role": "user", "content": "Hello"}, "user"),
(
{"role": "ai", "content": "Hi there!"},
"assistant",
), # Should be mapped to "assistant"
(
{"role": "assistant", "content": "How can I help?"},
"assistant",
), # Should stay "assistant"
],
)
def test_anthropic_message_role_mapping(
sentry_init,
capture_events,
capture_items,
test_message,
expected_role,
stream_gen_ai_spans,
span_streaming,
):
"""Test that Anthropic integration properly maps message roles like 'ai' to 'assistant'"""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
def mock_messages_create(*args, **kwargs):
return Message(
id="msg_1",
content=[TextBlock(text="Hi there!", type="text")],
model="claude-3-opus",
role="assistant",
stop_reason="end_turn",
stop_sequence=None,
type="message",
usage=Usage(input_tokens=10, output_tokens=5),
)
client.messages._post = mock.Mock(return_value=mock_messages_create())
test_messages = [test_message]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic tx"):
client.messages.create(
model="claude-3-opus", max_tokens=10, messages=test_messages
)
sentry_sdk.flush()
span = next(item.payload for item in items if item.type == "span")
# Verify that the span was created correctly
assert span["attributes"]["sentry.op"] == "gen_ai.chat"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
# Parse the stored messages
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic tx"):
client.messages.create(
model="claude-3-opus", max_tokens=10, messages=test_messages
)
(event,) = events
span = event["spans"][0]
# Verify that the span was created correctly
assert span["op"] == "gen_ai.chat"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
# Parse the stored messages
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert stored_messages[0]["role"] == expected_role
def test_anthropic_message_truncation(sentry_init, capture_events):
"""Test that large messages are truncated properly in Anthropic integration."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
large_content = (
"This is a very long message that will exceed our size limits. " * 1000
)
messages = [
{"role": "user", "content": "small message 1"},
{"role": "assistant", "content": large_content},
{"role": "user", "content": large_content},
{"role": "assistant", "content": "small message 4"},
{"role": "user", "content": "small message 5"},
]
with start_transaction():
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) > 0
tx = events[0]
assert tx["type"] == "transaction"
chat_spans = [
span for span in tx.get("spans", []) if span.get("op") == OP.GEN_AI_CHAT
]
assert len(chat_spans) > 0
chat_span = chat_spans[0]
assert chat_span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert chat_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in chat_span["data"]
messages_data = chat_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert isinstance(messages_data, str)
parsed_messages = json.loads(messages_data)
assert isinstance(parsed_messages, list)
assert len(parsed_messages) == 1
assert "small message 5" in str(parsed_messages[0])
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
@pytest.mark.asyncio
async def test_anthropic_message_truncation_async(sentry_init, capture_events):
"""Test that large messages are truncated properly in Anthropic integration."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = AsyncAnthropic(api_key="z")
client.messages._post = mock.AsyncMock(return_value=EXAMPLE_MESSAGE)
large_content = (
"This is a very long message that will exceed our size limits. " * 1000
)
messages = [
{"role": "user", "content": "small message 1"},
{"role": "assistant", "content": large_content},
{"role": "user", "content": large_content},
{"role": "assistant", "content": "small message 4"},
{"role": "user", "content": "small message 5"},
]
with start_transaction():
await client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) > 0
tx = events[0]
assert tx["type"] == "transaction"
chat_spans = [
span for span in tx.get("spans", []) if span.get("op") == OP.GEN_AI_CHAT
]
assert len(chat_spans) > 0
chat_span = chat_spans[0]
assert chat_span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert chat_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in chat_span["data"]
messages_data = chat_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
assert isinstance(messages_data, str)
parsed_messages = json.loads(messages_data)
assert isinstance(parsed_messages, list)
assert len(parsed_messages) == 1
assert "small message 5" in str(parsed_messages[0])
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_nonstreaming_create_message_with_system_prompt(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in GEN_AI_REQUEST_MESSAGES."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"end_turn"
]
else:
events = capture_events()
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_nonstreaming_create_message_with_system_prompt_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in GEN_AI_REQUEST_MESSAGES (async)."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with start_transaction(name="anthropic"):
response = await client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"end_turn"
]
else:
events = capture_events()
with start_transaction(name="anthropic"):
response = await client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
)
assert response == EXAMPLE_MESSAGE
usage = response.usage
assert usage.input_tokens == 10
assert usage.output_tokens == 20
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi, I'm Claude."
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is False
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_streaming_create_message_with_system_prompt(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in streaming mode."""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
stream=True,
system="You are a helpful assistant.",
)
for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
stream=True,
system="You are a helpful assistant.",
)
for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
def test_stream_messages_with_system_prompt(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in streaming mode."""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
) as stream:
for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
) as stream:
for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_stream_message_with_system_prompt_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in streaming mode (async)."""
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
) as stream:
async for event in stream:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
async with client.messages.stream(
max_tokens=1024,
messages=messages,
model="model",
system="You are a helpful assistant.",
) as stream:
async for event in stream:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[
(True, True),
(True, False),
(False, True),
(False, False),
],
)
async def test_streaming_create_message_with_system_prompt_async(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
get_model_response,
async_iterator,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test that system prompts are properly captured in streaming mode (async)."""
client = AsyncAnthropic(api_key="z")
response = get_model_response(
async_iterator(
server_side_event_chunks(
[
MessageStartEvent(
message=EXAMPLE_MESSAGE,
type="message_start",
),
ContentBlockStartEvent(
type="content_block_start",
index=0,
content_block=TextBlock(type="text", text=""),
),
ContentBlockDeltaEvent(
delta=TextDelta(text="Hi", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text="!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockDeltaEvent(
delta=TextDelta(text=" I'm Claude!", type="text_delta"),
index=0,
type="content_block_delta",
),
ContentBlockStopEvent(type="content_block_stop", index=0),
MessageDeltaEvent(
delta=Delta(),
usage=MessageDeltaUsage(output_tokens=10),
type="message_delta",
),
]
)
)
)
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("transaction", "span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
stream=True,
system="You are a helpful assistant.",
)
async for _ in message:
pass
(event,) = (item.payload for item in items if item.type == "transaction")
assert event["transaction"] == "anthropic"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
(span,) = spans
assert span["attributes"]["sentry.op"] == OP.GEN_AI_CHAT
assert span["name"] == "chat model"
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["attributes"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert (
span["attributes"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
)
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["attributes"]
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024,
messages=messages,
model="model",
stream=True,
system="You are a helpful assistant.",
)
async for _ in message:
pass
assert len(events) == 1
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "anthropic"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GEN_AI_CHAT
assert span["description"] == "chat model"
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert span["data"][SPANDATA.GEN_AI_REQUEST_MODEL] == "model"
if send_default_pii and include_prompts:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."}
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello, Claude"
assert span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT] == "Hi! I'm Claude!"
else:
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
assert SPANDATA.GEN_AI_RESPONSE_TEXT not in span["data"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_RESPONSE_STREAMING] is True
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_system_prompt_with_complex_structure(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that complex system prompt structures (list of text blocks) are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
# System prompt as list of text blocks
system_prompt = [
{"type": "text", "text": "You are a helpful assistant."},
{"type": "text", "text": "Be concise and clear."},
]
messages = [
{
"role": "user",
"content": "Hello",
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024, messages=messages, model="model", system=system_prompt
)
assert response == EXAMPLE_MESSAGE
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 1
(span,) = spans
assert span["attributes"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["attributes"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["attributes"]
system_instructions = json.loads(
span["attributes"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
# System content should be a list of text blocks
assert isinstance(system_instructions, list)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."},
{"type": "text", "content": "Be concise and clear."},
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["attributes"]
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
response = client.messages.create(
max_tokens=1024, messages=messages, model="model", system=system_prompt
)
assert response == EXAMPLE_MESSAGE
assert len(events) == 1
(event,) = events
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["data"][SPANDATA.GEN_AI_SYSTEM] == "anthropic"
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS in span["data"]
system_instructions = json.loads(
span["data"][SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS]
)
# System content should be a list of text blocks
assert isinstance(system_instructions, list)
assert system_instructions == [
{"type": "text", "content": "You are a helpful assistant."},
{"type": "text", "content": "Be concise and clear."},
]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
assert stored_messages[0]["content"] == "Hello"
# Tests for transform_content_part (shared) and _transform_anthropic_content_block helper functions
def test_transform_content_part_anthropic_base64_image():
"""Test that base64 encoded images are transformed to blob format."""
content_block = {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64encodeddata...",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "blob",
"modality": "image",
"mime_type": "image/jpeg",
"content": "base64encodeddata...",
}
def test_transform_content_part_anthropic_url_image():
"""Test that URL-referenced images are transformed to uri format."""
content_block = {
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/image.jpg",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "uri",
"modality": "image",
"mime_type": "",
"uri": "https://example.com/image.jpg",
}
def test_transform_content_part_anthropic_file_image():
"""Test that file_id-referenced images are transformed to file format."""
content_block = {
"type": "image",
"source": {
"type": "file",
"file_id": "file_abc123",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "file",
"modality": "image",
"mime_type": "",
"file_id": "file_abc123",
}
def test_transform_content_part_anthropic_base64_document():
"""Test that base64 encoded PDFs are transformed to blob format."""
content_block = {
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "base64encodedpdfdata...",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "blob",
"modality": "document",
"mime_type": "application/pdf",
"content": "base64encodedpdfdata...",
}
def test_transform_content_part_anthropic_url_document():
"""Test that URL-referenced documents are transformed to uri format."""
content_block = {
"type": "document",
"source": {
"type": "url",
"url": "https://example.com/document.pdf",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "uri",
"modality": "document",
"mime_type": "",
"uri": "https://example.com/document.pdf",
}
def test_transform_content_part_anthropic_file_document():
"""Test that file_id-referenced documents are transformed to file format."""
content_block = {
"type": "document",
"source": {
"type": "file",
"file_id": "file_doc456",
"media_type": "application/pdf",
},
}
result = transform_content_part(content_block)
assert result == {
"type": "file",
"modality": "document",
"mime_type": "application/pdf",
"file_id": "file_doc456",
}
def test_transform_anthropic_content_block_text_document():
"""Test that plain text documents are transformed correctly (Anthropic-specific)."""
content_block = {
"type": "document",
"source": {
"type": "text",
"media_type": "text/plain",
"data": "This is plain text content.",
},
}
# Use Anthropic-specific helper for text-type documents
result = _transform_anthropic_content_block(content_block)
assert result == {
"type": "text",
"text": "This is plain text content.",
}
def test_transform_content_part_text_block():
"""Test that regular text blocks return None (not transformed)."""
content_block = {
"type": "text",
"text": "Hello, world!",
}
# Shared transform_content_part returns None for text blocks
result = transform_content_part(content_block)
assert result is None
def test_transform_message_content_string():
"""Test that string content is returned as-is."""
result = transform_message_content("Hello, world!")
assert result == "Hello, world!"
def test_transform_message_content_list_anthropic():
"""Test that list content with Anthropic format is transformed correctly."""
content = [
{"type": "text", "text": "Hello!"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "base64data...",
},
},
]
result = transform_message_content(content)
assert len(result) == 2
# Text block stays as-is (transform returns None, keeps original)
assert result[0] == {"type": "text", "text": "Hello!"}
assert result[1] == {
"type": "blob",
"modality": "image",
"mime_type": "image/png",
"content": "base64data...",
}
# Integration tests for binary data in messages
def test_message_with_base64_image(sentry_init, capture_events):
"""Test that messages with base64 images are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64encodeddatahere...",
},
},
],
}
]
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in span["data"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
assert len(stored_messages) == 1
assert stored_messages[0]["role"] == "user"
content = stored_messages[0]["content"]
assert len(content) == 2
assert content[0] == {"type": "text", "text": "What's in this image?"}
assert content[1] == {
"type": "blob",
"modality": "image",
"mime_type": "image/jpeg",
"content": BLOB_DATA_SUBSTITUTE,
}
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_message_with_url_image(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that messages with URL-referenced images are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image."},
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/photo.png",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert content[1] == {
"type": "uri",
"modality": "image",
"mime_type": "",
"uri": "https://example.com/photo.png",
}
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_message_with_file_image(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that messages with file_id-referenced images are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What do you see?"},
{
"type": "image",
"source": {
"type": "file",
"file_id": "file_img_12345",
"media_type": "image/webp",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert content[1] == {
"type": "file",
"modality": "image",
"mime_type": "image/webp",
"file_id": "file_img_12345",
}
def test_message_with_base64_pdf(sentry_init, capture_events):
"""Test that messages with base64-encoded PDF documents are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this document."},
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "JVBERi0xLjQKJeLj...base64pdfdata",
},
},
],
}
]
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert content[1] == {
"type": "blob",
"modality": "document",
"mime_type": "application/pdf",
"content": BLOB_DATA_SUBSTITUTE,
}
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_message_with_url_pdf(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that messages with URL-referenced PDF documents are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What is in this PDF?"},
{
"type": "document",
"source": {
"type": "url",
"url": "https://example.com/report.pdf",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert content[1] == {
"type": "uri",
"modality": "document",
"mime_type": "",
"uri": "https://example.com/report.pdf",
}
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_message_with_file_document(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that messages with file_id-referenced documents are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze this document."},
{
"type": "document",
"source": {
"type": "file",
"file_id": "file_doc_67890",
"media_type": "application/pdf",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
stored_messages = json.loads(
span["attributes"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert content[1] == {
"type": "file",
"modality": "document",
"mime_type": "application/pdf",
"file_id": "file_doc_67890",
}
def test_message_with_mixed_content(sentry_init, capture_events):
"""Test that messages with mixed content (text, images, documents) are properly captured."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Compare this image with the document."},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgo...base64imagedata",
},
},
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/comparison.jpg",
},
},
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "JVBERi0xLjQK...base64pdfdata",
},
},
{"type": "text", "text": "Please provide a detailed analysis."},
],
}
]
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert len(content) == 5
assert content[0] == {
"type": "text",
"text": "Compare this image with the document.",
}
assert content[1] == {
"type": "blob",
"modality": "image",
"mime_type": "image/png",
"content": BLOB_DATA_SUBSTITUTE,
}
assert content[2] == {
"type": "uri",
"modality": "image",
"mime_type": "",
"uri": "https://example.com/comparison.jpg",
}
assert content[3] == {
"type": "blob",
"modality": "document",
"mime_type": "application/pdf",
"content": BLOB_DATA_SUBSTITUTE,
}
assert content[4] == {
"type": "text",
"text": "Please provide a detailed analysis.",
}
def test_message_with_multiple_images_different_formats(sentry_init, capture_events):
"""Test that messages with multiple images of different source types are handled."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=False,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64data1...",
},
},
{
"type": "image",
"source": {
"type": "url",
"url": "https://example.com/img2.gif",
},
},
{
"type": "image",
"source": {
"type": "file",
"file_id": "file_img_789",
"media_type": "image/webp",
},
},
{"type": "text", "text": "Compare these three images."},
],
}
]
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
content = stored_messages[0]["content"]
assert len(content) == 4
assert content[0] == {
"type": "blob",
"modality": "image",
"mime_type": "image/jpeg",
"content": BLOB_DATA_SUBSTITUTE,
}
assert content[1] == {
"type": "uri",
"modality": "image",
"mime_type": "",
"uri": "https://example.com/img2.gif",
}
assert content[2] == {
"type": "file",
"modality": "image",
"mime_type": "image/webp",
"file_id": "file_img_789",
}
assert content[3] == {"type": "text", "text": "Compare these three images."}
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_binary_content_not_stored_when_pii_disabled(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that binary content is not stored when send_default_pii is False."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=False,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64encodeddatahere...",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
# Messages should not be stored
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
# Messages should not be stored
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_binary_content_not_stored_when_prompts_disabled(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test that binary content is not stored when include_prompts is False."""
sentry_init(
integrations=[AnthropicIntegration(include_prompts=False)],
traces_sample_rate=1.0,
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64encodeddatahere...",
},
},
],
}
]
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = spans
# Messages should not be stored
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"]
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(max_tokens=1024, messages=messages, model="model")
assert len(events) == 1
(event,) = events
(span,) = event["spans"]
# Messages should not be stored
assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"]
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_cache_tokens_nonstreaming(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""Test cache read/write tokens are tracked for non-streaming responses."""
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
return_value=Message(
id="id",
model="claude-3-5-sonnet-20241022",
role="assistant",
content=[TextBlock(type="text", text="Response")],
type="message",
usage=Usage(
input_tokens=100,
output_tokens=50,
cache_read_input_tokens=80,
cache_creation_input_tokens=20,
),
)
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
)
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 50
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 250
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
)
(span,) = events[0]["spans"]
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 50
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 250
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_input_tokens_include_cache_write_nonstreaming(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""
Test that gen_ai.usage.input_tokens includes cache_write tokens (non-streaming).
Reproduces a real Anthropic cache-write response. Anthropic's usage.input_tokens
only counts non-cached tokens, but gen_ai.usage.input_tokens should be the TOTAL
so downstream cost calculations don't produce negative values.
Real Anthropic response (from E2E test):
Usage(input_tokens=19, output_tokens=14,
cache_creation_input_tokens=2846, cache_read_input_tokens=0)
"""
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
return_value=Message(
id="id",
model="claude-sonnet-4-20250514",
role="assistant",
content=[TextBlock(type="text", text="3 + 3 equals 6.")],
type="message",
usage=Usage(
input_tokens=19,
output_tokens=14,
cache_read_input_tokens=0,
cache_creation_input_tokens=2846,
),
)
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 3+3?"}],
model="claude-sonnet-4-20250514",
)
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens should be total: 19 (non-cached) + 2846 (cache_write) = 2865
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert (
span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879
) # 2865 + 14
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 0
assert (
span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 2846
)
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 3+3?"}],
model="claude-sonnet-4-20250514",
)
(span,) = events[0]["spans"]
# input_tokens should be total: 19 (non-cached) + 2846 (cache_write) = 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 0
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 2846
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_input_tokens_include_cache_read_nonstreaming(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""
Test that gen_ai.usage.input_tokens includes cache_read tokens (non-streaming).
Reproduces a real Anthropic cache-hit response. This is the scenario that
caused negative gen_ai.cost.input_tokens: input_tokens=19 but cached=2846,
so the backend computed 19 - 2846 = -2827 "regular" tokens.
Real Anthropic response (from E2E test):
Usage(input_tokens=19, output_tokens=14,
cache_creation_input_tokens=0, cache_read_input_tokens=2846)
"""
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
return_value=Message(
id="id",
model="claude-sonnet-4-20250514",
role="assistant",
content=[TextBlock(type="text", text="5 + 5 = 10.")],
type="message",
usage=Usage(
input_tokens=19,
output_tokens=14,
cache_read_input_tokens=2846,
cache_creation_input_tokens=0,
),
)
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
)
sentry_sdk.flush()
(span,) = [item.payload for item in items]
# input_tokens should be total: 19 (non-cached) + 2846 (cache_read) = 2865
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert (
span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879
) # 2865 + 14
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
)
(span,) = events[0]["spans"]
# input_tokens should be total: 19 (non-cached) + 2846 (cache_read) = 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_input_tokens_include_cache_read_streaming(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""
Test that gen_ai.usage.input_tokens includes cache_read tokens (streaming).
Same cache-hit scenario as non-streaming, using realistic streaming events.
"""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
type="message_start",
message=Message(
id="id",
model="claude-sonnet-4-20250514",
role="assistant",
content=[],
type="message",
usage=Usage(
input_tokens=19,
output_tokens=0,
cache_read_input_tokens=2846,
cache_creation_input_tokens=0,
),
),
),
MessageDeltaEvent(
type="message_delta",
delta=Delta(stop_reason="end_turn"),
usage=MessageDeltaUsage(output_tokens=14),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
for _ in client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
stream=True,
):
pass
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens should be total: 19 + 2846 = test_stream_messages_input_tokens_include_cache_read_streaming
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert (
span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879
) # 2865 + 14
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
for _ in client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
stream=True,
):
pass
(span,) = events[0]["spans"]
# input_tokens should be total: 19 + 2846 = test_stream_messages_input_tokens_include_cache_read_streaming
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_stream_messages_input_tokens_include_cache_read_streaming(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""
Test that gen_ai.usage.input_tokens includes cache_read tokens (streaming).
Same cache-hit scenario as non-streaming, using realistic streaming events.
"""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
type="message_start",
message=Message(
id="id",
model="claude-sonnet-4-20250514",
role="assistant",
content=[],
type="message",
usage=Usage(
input_tokens=19,
output_tokens=0,
cache_read_input_tokens=2846,
cache_creation_input_tokens=0,
),
),
),
MessageDeltaEvent(
type="message_delta",
delta=Delta(stop_reason="end_turn"),
usage=MessageDeltaUsage(output_tokens=14),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
) as stream:
for event in stream:
pass
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens should be total: 19 + 2846 = 2865
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert (
span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879
) # 2865 + 14
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 5+5?"}],
model="claude-sonnet-4-20250514",
) as stream:
for event in stream:
pass
(span,) = events[0]["spans"]
# input_tokens should be total: 19 + 2846 = 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 2865
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 2879 # 2865 + 14
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 2846
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 0
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_input_tokens_unchanged_without_caching(
sentry_init,
capture_events,
capture_items,
stream_gen_ai_spans,
span_streaming,
):
"""
Test that input_tokens is unchanged when there are no cached tokens.
Real Anthropic response (from E2E test, simple call without caching):
Usage(input_tokens=20, output_tokens=12)
"""
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(
return_value=Message(
id="id",
model="claude-sonnet-4-20250514",
role="assistant",
content=[TextBlock(type="text", text="2+2 equals 4.")],
type="message",
usage=Usage(
input_tokens=20,
output_tokens=12,
),
)
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 2+2?"}],
model="claude-sonnet-4-20250514",
)
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 32 # 20 + 12
else:
events = capture_events()
with start_transaction(name="anthropic"):
client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "What is 2+2?"}],
model="claude-sonnet-4-20250514",
)
(span,) = events[0]["spans"]
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 32 # 20 + 12
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_cache_tokens_streaming(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test cache tokens are tracked for streaming responses."""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
type="message_start",
message=Message(
id="id",
model="claude-3-5-sonnet-20241022",
role="assistant",
content=[],
type="message",
usage=Usage(
input_tokens=100,
output_tokens=0,
cache_read_input_tokens=80,
cache_creation_input_tokens=20,
),
),
),
MessageDeltaEvent(
type="message_delta",
delta=Delta(stop_reason="end_turn"),
usage=MessageDeltaUsage(output_tokens=10),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
for _ in client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
stream=True,
):
pass
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"):
for _ in client.messages.create(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
stream=True,
):
pass
(span,) = events[0]["spans"]
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_stream_messages_cache_tokens(
sentry_init,
capture_events,
capture_items,
get_model_response,
server_side_event_chunks,
stream_gen_ai_spans,
span_streaming,
):
"""Test cache tokens are tracked for streaming responses."""
client = Anthropic(api_key="z")
response = get_model_response(
server_side_event_chunks(
[
MessageStartEvent(
type="message_start",
message=Message(
id="id",
model="claude-3-5-sonnet-20241022",
role="assistant",
content=[],
type="message",
usage=Usage(
input_tokens=100,
output_tokens=0,
cache_read_input_tokens=80,
cache_creation_input_tokens=20,
),
),
),
MessageDeltaEvent(
type="message_delta",
delta=Delta(stop_reason="end_turn"),
usage=MessageDeltaUsage(output_tokens=10),
),
]
)
)
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
stream_gen_ai_spans=stream_gen_ai_spans,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming or stream_gen_ai_spans:
items = capture_items("span")
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
) as stream:
for event in stream:
pass
sentry_sdk.flush()
(span,) = (item.payload for item in items if item.type == "span")
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
else:
events = capture_events()
with mock.patch.object(
client._client,
"send",
return_value=response,
) as _, start_transaction(name="anthropic"), client.messages.stream(
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
model="claude-3-5-sonnet-20241022",
) as stream:
for event in stream:
pass
(span,) = events[0]["spans"]
# input_tokens normalized: 100 + 80 (cache_read) + 20 (cache_write) = 200
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 200
assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 210
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED] == 80
assert span["data"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE] == 20
sentry-python-2.64.0/tests/integrations/argv/ 0000775 0000000 0000000 00000000000 15220673775 0021273 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/argv/test_argv.py 0000664 0000000 0000000 00000000644 15220673775 0023647 0 ustar 00root root 0000000 0000000 import sys
from sentry_sdk import capture_message
from sentry_sdk.integrations.argv import ArgvIntegration
def test_basic(sentry_init, capture_events, monkeypatch):
sentry_init(integrations=[ArgvIntegration()])
argv = ["foo", "bar", "baz"]
monkeypatch.setattr(sys, "argv", argv)
events = capture_events()
capture_message("hi")
(event,) = events
assert event["extra"]["sys.argv"] == argv
sentry-python-2.64.0/tests/integrations/ariadne/ 0000775 0000000 0000000 00000000000 15220673775 0021737 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/ariadne/__init__.py 0000664 0000000 0000000 00000000152 15220673775 0024046 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("ariadne")
pytest.importorskip("fastapi")
pytest.importorskip("flask")
sentry-python-2.64.0/tests/integrations/ariadne/test_ariadne.py 0000664 0000000 0000000 00000016504 15220673775 0024761 0 ustar 00root root 0000000 0000000 from ariadne import ObjectType, QueryType, gql, graphql_sync, make_executable_schema
from ariadne.asgi import GraphQL
from fastapi import FastAPI
from fastapi.testclient import TestClient
from flask import Flask, jsonify, request
from sentry_sdk.integrations.ariadne import AriadneIntegration
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
def schema_factory():
type_defs = gql(
"""
type Query {
greeting(name: String): Greeting
error: String
}
type Greeting {
name: String
}
"""
)
query = QueryType()
greeting = ObjectType("Greeting")
@query.field("greeting")
def resolve_greeting(*_, **kwargs):
name = kwargs.pop("name")
return {"name": name}
@query.field("error")
def resolve_error(obj, *_):
raise RuntimeError("resolver failed")
@greeting.field("name")
def resolve_name(obj, *_):
return "Hello, {}!".format(obj["name"])
return make_executable_schema(type_defs, query)
def test_capture_request_and_response_if_send_pii_is_on_async(
sentry_init, capture_events
):
sentry_init(
send_default_pii=True,
integrations=[
AriadneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = schema_factory()
async_app = FastAPI()
async_app.mount("/graphql/", GraphQL(schema))
query = {"query": "query ErrorQuery {error}"}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "ariadne"
assert event["contexts"]["response"] == {
"data": {
"data": {"error": None},
"errors": [
{
"locations": [{"column": 19, "line": 1}],
"message": "resolver failed",
"path": ["error"],
}
],
}
}
assert event["request"]["api_target"] == "graphql"
assert event["request"]["data"] == query
def test_capture_request_and_response_if_send_pii_is_on_sync(
sentry_init, capture_events
):
sentry_init(
send_default_pii=True,
integrations=[AriadneIntegration(), FlaskIntegration()],
)
events = capture_events()
schema = schema_factory()
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server():
data = request.get_json()
success, result = graphql_sync(schema, data)
return jsonify(result), 200
query = {"query": "query ErrorQuery {error}"}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "ariadne"
assert event["contexts"]["response"] == {
"data": {
"data": {"error": None},
"errors": [
{
"locations": [{"column": 19, "line": 1}],
"message": "resolver failed",
"path": ["error"],
}
],
}
}
assert event["request"]["api_target"] == "graphql"
assert event["request"]["data"] == query
def test_do_not_capture_request_and_response_if_send_pii_is_off_async(
sentry_init, capture_events
):
sentry_init(
integrations=[
AriadneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = schema_factory()
async_app = FastAPI()
async_app.mount("/graphql/", GraphQL(schema))
query = {"query": "query ErrorQuery {error}"}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "ariadne"
assert "data" not in event["request"]
assert "response" not in event["contexts"]
def test_do_not_capture_request_and_response_if_send_pii_is_off_sync(
sentry_init, capture_events
):
sentry_init(
integrations=[AriadneIntegration(), FlaskIntegration()],
)
events = capture_events()
schema = schema_factory()
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server():
data = request.get_json()
success, result = graphql_sync(schema, data)
return jsonify(result), 200
query = {"query": "query ErrorQuery {error}"}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "ariadne"
assert "data" not in event["request"]
assert "response" not in event["contexts"]
def test_capture_validation_error(sentry_init, capture_events):
sentry_init(
send_default_pii=True,
integrations=[
AriadneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = schema_factory()
async_app = FastAPI()
async_app.mount("/graphql/", GraphQL(schema))
query = {"query": "query ErrorQuery {doesnt_exist}"}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "ariadne"
assert event["contexts"]["response"] == {
"data": {
"errors": [
{
"locations": [{"column": 19, "line": 1}],
"message": "Cannot query field 'doesnt_exist' on type 'Query'.",
}
]
}
}
assert event["request"]["api_target"] == "graphql"
assert event["request"]["data"] == query
def test_no_event_if_no_errors_async(sentry_init, capture_events):
sentry_init(
integrations=[
AriadneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = schema_factory()
async_app = FastAPI()
async_app.mount("/graphql/", GraphQL(schema))
query = {
"query": "query GreetingQuery($name: String) { greeting(name: $name) {name} }",
"variables": {"name": "some name"},
}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 0
def test_no_event_if_no_errors_sync(sentry_init, capture_events):
sentry_init(
integrations=[AriadneIntegration(), FlaskIntegration()],
)
events = capture_events()
schema = schema_factory()
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server():
data = request.get_json()
success, result = graphql_sync(schema, data)
return jsonify(result), 200
query = {
"query": "query GreetingQuery($name: String) { greeting(name: $name) {name} }",
"variables": {"name": "some name"},
}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 0
sentry-python-2.64.0/tests/integrations/arq/ 0000775 0000000 0000000 00000000000 15220673775 0021117 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/arq/__init__.py 0000664 0000000 0000000 00000000052 15220673775 0023225 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("arq")
sentry-python-2.64.0/tests/integrations/arq/test_arq.py 0000664 0000000 0000000 00000046621 15220673775 0023324 0 ustar 00root root 0000000 0000000 import asyncio
from datetime import timedelta
import arq.worker
import pytest
from arq import cron
from arq.connections import ArqRedis
from arq.jobs import Job
from arq.utils import timestamp_ms
from fakeredis.aioredis import FakeRedis
import sentry_sdk
from sentry_sdk import get_client, start_transaction
from sentry_sdk.integrations.arq import ArqIntegration
def async_partial(async_fn, *args, **kwargs):
# asyncio.iscoroutinefunction (Used in the integration code) in Python < 3.8
# does not detect async functions in functools.partial objects.
# This partial implementation returns a coroutine instead.
async def wrapped(ctx):
return await async_fn(ctx, *args, **kwargs)
return wrapped
@pytest.fixture(autouse=True)
def patch_fakeredis_info_command():
from fakeredis._fakesocket import FakeSocket
if not hasattr(FakeSocket, "info"):
from fakeredis._commands import command
from fakeredis._helpers import SimpleString
@command((SimpleString,), name="info")
def info(self, section):
return section
FakeSocket.info = info
@pytest.fixture
def init_arq(sentry_init):
def inner(
span_streaming,
cls_functions=None,
cls_cron_jobs=None,
kw_functions=None,
kw_cron_jobs=None,
allow_abort_jobs_=False,
):
cls_functions = cls_functions or []
cls_cron_jobs = cls_cron_jobs or []
kwargs = {}
if kw_functions is not None:
kwargs["functions"] = kw_functions
if kw_cron_jobs is not None:
kwargs["cron_jobs"] = kw_cron_jobs
sentry_init(
integrations=[ArqIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
server = FakeRedis()
pool = ArqRedis(pool_or_conn=server.connection_pool)
class WorkerSettings:
functions = cls_functions
cron_jobs = cls_cron_jobs
redis_pool = pool
allow_abort_jobs = allow_abort_jobs_
if not WorkerSettings.functions:
del WorkerSettings.functions
if not WorkerSettings.cron_jobs:
del WorkerSettings.cron_jobs
worker = arq.worker.create_worker(WorkerSettings, **kwargs)
return pool, worker
return inner
@pytest.fixture
def init_arq_with_dict_settings(sentry_init):
def inner(
span_streaming,
cls_functions=None,
cls_cron_jobs=None,
kw_functions=None,
kw_cron_jobs=None,
allow_abort_jobs_=False,
):
cls_functions = cls_functions or []
cls_cron_jobs = cls_cron_jobs or []
kwargs = {}
if kw_functions is not None:
kwargs["functions"] = kw_functions
if kw_cron_jobs is not None:
kwargs["cron_jobs"] = kw_cron_jobs
sentry_init(
integrations=[ArqIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
server = FakeRedis()
pool = ArqRedis(pool_or_conn=server.connection_pool)
worker_settings = {
"functions": cls_functions,
"cron_jobs": cls_cron_jobs,
"redis_pool": pool,
"allow_abort_jobs": allow_abort_jobs_,
}
if not worker_settings["functions"]:
del worker_settings["functions"]
if not worker_settings["cron_jobs"]:
del worker_settings["cron_jobs"]
worker = arq.worker.create_worker(worker_settings, **kwargs)
return pool, worker
return inner
@pytest.fixture
def init_arq_with_kwarg_settings(sentry_init):
"""Test fixture that passes settings_cls as keyword argument only."""
def inner(
span_streaming,
cls_functions=None,
cls_cron_jobs=None,
kw_functions=None,
kw_cron_jobs=None,
allow_abort_jobs_=False,
):
cls_functions = cls_functions or []
cls_cron_jobs = cls_cron_jobs or []
kwargs = {}
if kw_functions is not None:
kwargs["functions"] = kw_functions
if kw_cron_jobs is not None:
kwargs["cron_jobs"] = kw_cron_jobs
sentry_init(
integrations=[ArqIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
server = FakeRedis()
pool = ArqRedis(pool_or_conn=server.connection_pool)
class WorkerSettings:
functions = cls_functions
cron_jobs = cls_cron_jobs
redis_pool = pool
allow_abort_jobs = allow_abort_jobs_
if not WorkerSettings.functions:
del WorkerSettings.functions
if not WorkerSettings.cron_jobs:
del WorkerSettings.cron_jobs
# Pass settings_cls as keyword argument (not positional)
worker = arq.worker.create_worker(settings_cls=WorkerSettings, **kwargs)
return pool, worker
return inner
@pytest.mark.asyncio
@pytest.mark.parametrize(
"init_arq_settings",
["init_arq", "init_arq_with_dict_settings", "init_arq_with_kwarg_settings"],
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_job_result(init_arq_settings, request, span_streaming):
async def increase(ctx, num):
return num + 1
init_fixture_method = request.getfixturevalue(init_arq_settings)
increase.__qualname__ = increase.__name__
pool, worker = init_fixture_method(span_streaming, [increase])
job = await pool.enqueue_job("increase", 3)
assert isinstance(job, Job)
await worker.run_job(job.job_id, timestamp_ms())
result = await job.result()
job_result = await job.result_info()
assert result == 4
assert job_result.result == 4
@pytest.mark.asyncio
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_job_retry(
capture_events,
capture_items,
init_arq_settings,
request,
span_streaming,
):
async def retry_job(ctx):
if ctx["job_try"] < 2:
raise arq.worker.Retry
init_fixture_method = request.getfixturevalue(init_arq_settings)
retry_job.__qualname__ = retry_job.__name__
pool, worker = init_fixture_method(span_streaming, [retry_job])
job = await pool.enqueue_job("retry_job")
if span_streaming:
items = capture_items("span")
await worker.run_job(job.job_id, timestamp_ms())
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[4]["attributes"]["sentry.op"] == "queue.task.arq"
assert spans[4]["status"] == "ok"
assert spans[4]["name"] == "retry_job"
await worker.run_job(job.job_id, timestamp_ms())
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[7]["attributes"]["sentry.op"] == "queue.task.arq"
assert spans[7]["status"] == "ok"
assert spans[7]["name"] == "retry_job"
else:
events = capture_events()
await worker.run_job(job.job_id, timestamp_ms())
event = events.pop(0)
assert event["contexts"]["trace"]["status"] == "aborted"
assert event["transaction"] == "retry_job"
assert event["tags"]["arq_task_id"] == job.job_id
assert event["extra"]["arq-job"]["retry"] == 1
await worker.run_job(job.job_id, timestamp_ms())
event = events.pop(0)
assert event["contexts"]["trace"]["status"] == "ok"
assert event["transaction"] == "retry_job"
assert event["tags"]["arq_task_id"] == job.job_id
assert event["extra"]["arq-job"]["retry"] == 2
@pytest.mark.parametrize(
"source", [("cls_functions", "cls_cron_jobs"), ("kw_functions", "kw_cron_jobs")]
)
@pytest.mark.parametrize("job_fails", [True, False], ids=["error", "success"])
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_job_transaction(
capture_events,
capture_items,
init_arq_settings,
source,
job_fails,
request,
span_streaming,
):
async def division(_, a, b=0):
return a / b
init_fixture_method = request.getfixturevalue(init_arq_settings)
division.__qualname__ = division.__name__
cron_func = async_partial(division, a=1, b=int(not job_fails))
cron_func.__qualname__ = division.__name__
cron_job = cron(cron_func, minute=0, run_at_startup=True)
functions_key, cron_jobs_key = source
pool, worker = init_fixture_method(
span_streaming, **{functions_key: [division], cron_jobs_key: [cron_job]}
)
job = await pool.enqueue_job("division", 1, b=int(not job_fails))
if span_streaming:
items = capture_items("event", "span")
await worker.run_job(job.job_id, timestamp_ms())
loop = asyncio.get_event_loop()
task = loop.create_task(worker.async_run())
await asyncio.sleep(1)
task.cancel()
await worker.close()
events = [item.payload for item in items if item.type == "event"]
if job_fails:
error_func_event = events.pop(0)
error_cron_event = events.pop(0)
assert (
error_func_event["exception"]["values"][0]["type"]
== "ZeroDivisionError"
)
assert (
error_func_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
)
func_extra = error_func_event["extra"]["arq-job"]
assert func_extra["task"] == "division"
assert (
error_cron_event["exception"]["values"][0]["type"]
== "ZeroDivisionError"
)
assert (
error_cron_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
)
cron_extra = error_cron_event["extra"]["arq-job"]
assert cron_extra["task"] == "cron:division"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
task_spans = [
span
for span in spans
if span["attributes"].get("sentry.op") == "queue.task.arq"
]
division_span = next(span for span in task_spans if span["name"] == "division")
assert division_span["attributes"]["sentry.span.source"] == "task"
assert any(span["name"] == "cron:division" for span in task_spans)
else:
events = capture_events()
await worker.run_job(job.job_id, timestamp_ms())
loop = asyncio.get_event_loop()
task = loop.create_task(worker.async_run())
await asyncio.sleep(1)
task.cancel()
await worker.close()
if job_fails:
error_func_event = events.pop(0)
error_cron_event = events.pop(1)
assert (
error_func_event["exception"]["values"][0]["type"]
== "ZeroDivisionError"
)
assert (
error_func_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
)
func_extra = error_func_event["extra"]["arq-job"]
assert func_extra["task"] == "division"
assert (
error_cron_event["exception"]["values"][0]["type"]
== "ZeroDivisionError"
)
assert (
error_cron_event["exception"]["values"][0]["mechanism"]["type"] == "arq"
)
cron_extra = error_cron_event["extra"]["arq-job"]
assert cron_extra["task"] == "cron:division"
[func_event, cron_event] = events
assert func_event["type"] == "transaction"
assert func_event["transaction"] == "division"
assert func_event["transaction_info"] == {"source": "task"}
assert "arq_task_id" in func_event["tags"]
assert "arq_task_retry" in func_event["tags"]
func_extra = func_event["extra"]["arq-job"]
assert func_extra["task"] == "division"
assert func_extra["kwargs"] == {"b": int(not job_fails)}
assert func_extra["retry"] == 1
assert cron_event["type"] == "transaction"
assert cron_event["transaction"] == "cron:division"
assert cron_event["transaction_info"] == {"source": "task"}
assert "arq_task_id" in cron_event["tags"]
assert "arq_task_retry" in cron_event["tags"]
cron_extra = cron_event["extra"]["arq-job"]
assert cron_extra["task"] == "cron:division"
assert cron_extra["kwargs"] == {}
assert cron_extra["retry"] == 1
@pytest.mark.parametrize("source", ["cls_functions", "kw_functions"])
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_enqueue_job(
capture_events,
capture_items,
init_arq_settings,
source,
request,
span_streaming,
):
async def dummy_job(_):
pass
init_fixture_method = request.getfixturevalue(init_arq_settings)
pool, _ = init_fixture_method(span_streaming, **{source: [dummy_job]})
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent") as span:
await pool.enqueue_job("dummy_job")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[2]["is_segment"] is True
assert spans[2]["trace_id"] == span.trace_id
assert spans[2]["span_id"] == span.span_id
assert spans[1]["attributes"]["sentry.op"] == "queue.submit.arq"
assert spans[1]["name"] == "dummy_job"
else:
events = capture_events()
with start_transaction() as transaction:
await pool.enqueue_job("dummy_job")
(event,) = events
assert event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert event["contexts"]["trace"]["span_id"] == transaction.span_id
assert len(event["spans"])
assert event["spans"][0]["op"] == "queue.submit.arq"
assert event["spans"][0]["description"] == "dummy_job"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_execute_job_without_integration(
init_arq_settings, request, span_streaming
):
async def dummy_job(_ctx):
pass
init_fixture_method = request.getfixturevalue(init_arq_settings)
dummy_job.__qualname__ = dummy_job.__name__
pool, worker = init_fixture_method(span_streaming, [dummy_job])
# remove the integration to trigger the edge case
get_client().integrations.pop("arq")
job = await pool.enqueue_job("dummy_job")
await worker.run_job(job.job_id, timestamp_ms())
assert await job.result() is None
@pytest.mark.parametrize("source", ["cls_functions", "kw_functions"])
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_span_origin_producer(
capture_events,
capture_items,
init_arq_settings,
source,
request,
span_streaming,
):
async def dummy_job(_):
pass
init_fixture_method = request.getfixturevalue(init_arq_settings)
pool, _ = init_fixture_method(span_streaming, **{source: [dummy_job]})
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
await pool.enqueue_job("dummy_job")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[2]["attributes"]["sentry.origin"] == "manual"
assert spans[1]["attributes"]["sentry.origin"] == "auto.queue.arq"
else:
events = capture_events()
with start_transaction():
await pool.enqueue_job("dummy_job")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.queue.arq"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"init_arq_settings", ["init_arq", "init_arq_with_dict_settings"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_span_origin_consumer(
capture_events,
capture_items,
init_arq_settings,
request,
span_streaming,
):
async def job(ctx):
pass
init_fixture_method = request.getfixturevalue(init_arq_settings)
job.__qualname__ = job.__name__
pool, worker = init_fixture_method(span_streaming, [job])
if span_streaming:
job = await pool.enqueue_job("retry_job")
items = capture_items("span")
await worker.run_job(job.job_id, timestamp_ms())
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[4]["attributes"]["sentry.op"] == "queue.task.arq"
assert spans[4]["attributes"]["sentry.origin"] == "auto.queue.arq"
assert spans[3]["attributes"]["sentry.origin"] == "auto.db.redis"
assert spans[2]["attributes"]["sentry.origin"] == "auto.db.redis"
else:
job = await pool.enqueue_job("retry_job")
events = capture_events()
await worker.run_job(job.job_id, timestamp_ms())
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.queue.arq"
assert event["spans"][0]["origin"] == "auto.db.redis"
assert event["spans"][1]["origin"] == "auto.db.redis"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_job_concurrency(
capture_events,
capture_items,
init_arq,
span_streaming,
):
"""
10 - division starts
70 - sleepy starts
110 - division raises error
120 - sleepy finishes
"""
async def sleepy(_):
await asyncio.sleep(0.05)
async def division(_):
await asyncio.sleep(0.1)
return 1 / 0
sleepy.__qualname__ = sleepy.__name__
division.__qualname__ = division.__name__
pool, worker = init_arq(span_streaming, [sleepy, division])
await pool.enqueue_job(
"division", _job_id="123", _defer_by=timedelta(milliseconds=10)
)
await pool.enqueue_job(
"sleepy", _job_id="456", _defer_by=timedelta(milliseconds=70)
)
if span_streaming:
items = capture_items("event")
loop = asyncio.get_event_loop()
task = loop.create_task(worker.async_run())
await asyncio.sleep(1)
task.cancel()
await worker.close()
events = [item.payload for item in items]
exception_event = events[0]
assert exception_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert exception_event["transaction"] == "division"
else:
events = capture_events()
loop = asyncio.get_event_loop()
task = loop.create_task(worker.async_run())
await asyncio.sleep(1)
task.cancel()
await worker.close()
exception_event = events[1]
assert exception_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert exception_event["transaction"] == "division"
assert exception_event["extra"]["arq-job"]["task"] == "division"
sentry-python-2.64.0/tests/integrations/asgi/ 0000775 0000000 0000000 00000000000 15220673775 0021257 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asgi/__init__.py 0000664 0000000 0000000 00000000201 15220673775 0023361 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("asyncio")
pytest.importorskip("pytest_asyncio")
pytest.importorskip("async_asgi_testclient")
sentry-python-2.64.0/tests/integrations/asgi/test_asgi.py 0000664 0000000 0000000 00000072765 15220673775 0023634 0 ustar 00root root 0000000 0000000 from collections import Counter
import pytest
from async_asgi_testclient import TestClient
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations._asgi_common import _get_headers, _get_ip
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware, _looks_like_asgi3
from sentry_sdk.tracing import TransactionSource
@pytest.fixture
def asgi3_app():
async def app(scope, receive, send):
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
elif (
scope["type"] == "http"
and "route" in scope
and scope["route"] == "/trigger/error"
):
1 / 0
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
}
)
await send(
{
"type": "http.response.body",
"body": b"Hello, world!",
}
)
return app
@pytest.fixture
def asgi3_app_with_error():
async def send_with_error(event):
1 / 0
async def app(scope, receive, send):
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
... # Do some startup here!
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
... # Do some shutdown here!
await send({"type": "lifespan.shutdown.complete"})
return
else:
await send_with_error(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
}
)
await send_with_error(
{
"type": "http.response.body",
"body": b"Hello, world!",
}
)
return app
@pytest.fixture
def asgi3_app_with_error_and_msg():
async def app(scope, receive, send):
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
}
)
capture_message("Let's try dividing by 0")
1 / 0
await send(
{
"type": "http.response.body",
"body": b"Hello, world!",
}
)
return app
@pytest.fixture
def asgi3_ws_app():
def message():
capture_message("Some message to the world!")
raise ValueError("Oh no")
async def app(scope, receive, send):
await send(
{
"type": "websocket.send",
"text": message(),
}
)
return app
@pytest.fixture
def asgi3_custom_transaction_app():
async def app(scope, receive, send):
sentry_sdk.get_current_scope().set_transaction_name(
"foobar", source=TransactionSource.CUSTOM
)
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
],
}
)
await send(
{
"type": "http.response.body",
"body": b"Hello, world!",
}
)
return app
def test_invalid_transaction_style(asgi3_app):
with pytest.raises(ValueError) as exp:
SentryAsgiMiddleware(asgi3_app, transaction_style="URL")
assert (
str(exp.value)
== "Invalid value for transaction_style: URL (must be in ('endpoint', 'url'))"
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"should_send_pii",
[True, False],
)
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_capture_transaction(
sentry_init,
asgi3_app,
capture_events,
capture_items,
span_streaming,
should_send_pii,
):
sentry_init(
send_default_pii=should_send_pii,
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app)
async with TestClient(app) as client:
if span_streaming:
items = capture_items("span")
else:
events = capture_events()
await client.get("/some_url?somevalue=123")
sentry_sdk.flush()
if span_streaming:
assert len(items) == 1
span = items[0].payload
assert span["is_segment"] is True
assert span["name"] == "/some_url"
assert span["attributes"]["sentry.span.source"] == "url"
assert span["attributes"]["sentry.op"] == "http.server"
assert span["attributes"]["network.protocol.name"] == "http"
assert span["attributes"]["http.request.method"] == "GET"
assert span["attributes"]["http.request.header.host"] == "localhost"
assert span["attributes"]["http.request.header.remote-addr"] == "127.0.0.1"
assert (
span["attributes"]["http.request.header.user-agent"] == "ASGI-Test-Client"
)
if should_send_pii:
assert (
span["attributes"]["url.full"]
== "http://localhost/some_url?somevalue=123"
)
assert span["attributes"]["url.path"] == "/some_url"
assert span["attributes"]["http.query"] == "somevalue=123"
else:
(transaction_event,) = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "/some_url"
assert transaction_event["transaction_info"] == {"source": "url"}
assert transaction_event["contexts"]["trace"]["op"] == "http.server"
assert transaction_event["request"] == {
"headers": {
"host": "localhost",
"remote-addr": "127.0.0.1",
"user-agent": "ASGI-Test-Client",
},
"method": "GET",
"query_string": "somevalue=123",
"url": "http://localhost/some_url",
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_capture_transaction_with_error(
sentry_init,
asgi3_app_with_error,
capture_events,
capture_items,
DictionaryContaining, # noqa: N803
span_streaming,
):
sentry_init(
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app_with_error)
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
await client.get("/some_url")
sentry_sdk.flush()
if span_streaming:
assert len(items) == 2
assert items[0].type == "event"
assert items[1].type == "span"
error_event = items[0].payload
span_item = items[1].payload
else:
(error_event, transaction_event) = events
assert error_event["transaction"] == "/some_url"
assert error_event["transaction_info"] == {"source": "url"}
assert error_event["contexts"]["trace"]["op"] == "http.server"
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert error_event["exception"]["values"][0]["value"] == "division by zero"
assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asgi"
if span_streaming:
assert span_item["trace_id"] == error_event["contexts"]["trace"]["trace_id"]
assert span_item["span_id"] == error_event["contexts"]["trace"]["span_id"]
assert span_item.get("parent_span_id") == error_event["contexts"]["trace"].get(
"parent_span_id"
)
assert span_item["status"] == "error"
else:
assert transaction_event["type"] == "transaction"
assert transaction_event["contexts"]["trace"] == DictionaryContaining(
error_event["contexts"]["trace"]
)
assert transaction_event["contexts"]["trace"]["status"] == "internal_error"
assert transaction_event["transaction"] == error_event["transaction"]
assert transaction_event["request"] == error_event["request"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_has_trace_if_performance_enabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()
await client.get("/")
sentry_sdk.flush()
if span_streaming:
msg_event, error_event, span = items
assert msg_event.type == "event"
msg_event = msg_event.payload
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event.type == "event"
error_event = error_event.payload
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert span.type == "span"
span = span.payload
assert span["trace_id"] is not None
assert (
error_event["contexts"]["trace"]["trace_id"]
== msg_event["contexts"]["trace"]["trace_id"]
== span["trace_id"]
)
else:
msg_event, error_event, transaction_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
== msg_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.asyncio
async def test_has_trace_if_performance_disabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init()
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get("/")
msg_event, error_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
@pytest.mark.asyncio
async def test_trace_from_headers_if_performance_enabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()
await client.get("/", headers={"sentry-trace": sentry_trace_header})
sentry_sdk.flush()
if span_streaming:
msg_event, error_event, span = items
assert msg_event.type == "event"
msg_event = msg_event.payload
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event.type == "event"
error_event = error_event.payload
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert span.type == "span"
span = span.payload
assert span["trace_id"] is not None
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert span["trace_id"] == trace_id
else:
msg_event, error_event, transaction_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.asyncio
async def test_trace_from_headers_if_performance_disabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init()
app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
events = capture_events()
await client.get("/", headers={"sentry-trace": sentry_trace_header})
msg_event, error_event = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.asyncio
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_websocket(
sentry_init,
asgi3_ws_app,
capture_events,
capture_items,
request,
span_streaming,
):
sentry_init(
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
asgi3_ws_app = SentryAsgiMiddleware(asgi3_ws_app)
request_url = "/ws"
with pytest.raises(ValueError):
client = TestClient(asgi3_ws_app)
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()
async with client.websocket_connect(request_url) as ws:
await ws.receive_text()
sentry_sdk.flush()
if span_streaming:
msg_event, error_event, span = items
assert msg_event.type == "event"
msg_event = msg_event.payload
assert msg_event["transaction"] == request_url
assert msg_event["transaction_info"] == {"source": "url"}
assert msg_event["message"] == "Some message to the world!"
assert error_event.type == "event"
error_event = error_event.payload
(exc,) = error_event["exception"]["values"]
assert exc["type"] == "ValueError"
assert exc["value"] == "Oh no"
assert span.type == "span"
span = span.payload
assert span["name"] == request_url
assert span["attributes"]["sentry.span.source"] == "url"
else:
msg_event, error_event, transaction_event = events
assert msg_event["transaction"] == request_url
assert msg_event["transaction_info"] == {"source": "url"}
assert msg_event["message"] == "Some message to the world!"
(exc,) = error_event["exception"]["values"]
assert exc["type"] == "ValueError"
assert exc["value"] == "Oh no"
assert transaction_event["transaction"] == request_url
assert transaction_event["transaction_info"] == {"source": "url"}
@pytest.mark.asyncio
async def test_auto_session_tracking_with_aggregates(
sentry_init, asgi3_app, capture_envelopes
):
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
app = SentryAsgiMiddleware(asgi3_app)
scope = {
"endpoint": asgi3_app,
"client": ("127.0.0.1", 60457),
}
with pytest.raises(ZeroDivisionError):
envelopes = capture_envelopes()
async with TestClient(app, scope=scope) as client:
scope["route"] = "/some/fine/url"
await client.get("/some/fine/url")
scope["route"] = "/some/fine/url"
await client.get("/some/fine/url")
scope["route"] = "/trigger/error"
await client.get("/trigger/error")
sentry_sdk.flush()
count_item_types = Counter()
for envelope in envelopes:
count_item_types[envelope.items[0].type] += 1
assert count_item_types["transaction"] == 3
assert count_item_types["event"] == 1
assert count_item_types["sessions"] == 1
assert len(envelopes) == 5
session_aggregates = envelopes[-1].items[0].payload.json["aggregates"]
assert session_aggregates[0]["exited"] == 2
assert session_aggregates[0]["crashed"] == 1
assert len(session_aggregates) == 1
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
(
"/message",
"url",
"generic ASGI request",
"route",
),
(
"/message",
"endpoint",
"tests.integrations.asgi.test_asgi.asgi3_app..app",
"component",
),
],
)
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
@pytest.mark.asyncio
async def test_transaction_style(
sentry_init,
asgi3_app,
capture_events,
capture_items,
url,
transaction_style,
expected_transaction,
expected_source,
span_streaming,
):
sentry_init(
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
scope = {
"endpoint": asgi3_app,
"route": url,
"client": ("127.0.0.1", 60457),
}
async with TestClient(app, scope=scope) as client:
if span_streaming:
items = capture_items("span")
else:
events = capture_events()
await client.get(url)
sentry_sdk.flush()
if span_streaming:
assert len(items) == 1
span = items[0].payload
assert span["name"] == expected_transaction
assert span["attributes"]["sentry.span.source"] == expected_source
else:
(transaction_event,) = events
assert transaction_event["transaction"] == expected_transaction
assert transaction_event["transaction_info"] == {"source": expected_source}
def mock_asgi2_app():
pass
class MockAsgi2App:
def __call__():
pass
class MockAsgi3App(MockAsgi2App):
def __await__():
pass
async def __call__():
pass
def test_looks_like_asgi3(asgi3_app):
# branch: inspect.isclass(app)
assert _looks_like_asgi3(MockAsgi3App)
assert not _looks_like_asgi3(MockAsgi2App)
# branch: inspect.isfunction(app)
assert _looks_like_asgi3(asgi3_app)
assert not _looks_like_asgi3(mock_asgi2_app)
# breanch: else
asgi3 = MockAsgi3App()
assert _looks_like_asgi3(asgi3)
asgi2 = MockAsgi2App()
assert not _looks_like_asgi3(asgi2)
def test_get_ip_x_forwarded_for():
headers = [
(b"x-forwarded-for", b"8.8.8.8"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "8.8.8.8"
# x-forwarded-for overrides x-real-ip
headers = [
(b"x-forwarded-for", b"8.8.8.8"),
(b"x-real-ip", b"10.10.10.10"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "8.8.8.8"
# when multiple x-forwarded-for headers are, the first is taken
headers = [
(b"x-forwarded-for", b"5.5.5.5"),
(b"x-forwarded-for", b"6.6.6.6"),
(b"x-forwarded-for", b"7.7.7.7"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "5.5.5.5"
def test_get_ip_x_real_ip():
headers = [
(b"x-real-ip", b"10.10.10.10"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "10.10.10.10"
# x-forwarded-for overrides x-real-ip
headers = [
(b"x-forwarded-for", b"8.8.8.8"),
(b"x-real-ip", b"10.10.10.10"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "8.8.8.8"
def test_get_ip():
# if now headers are provided the ip is taken from the client.
headers = []
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "127.0.0.1"
# x-forwarded-for header overides the ip from client
headers = [
(b"x-forwarded-for", b"8.8.8.8"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "8.8.8.8"
# x-real-for header overides the ip from client
headers = [
(b"x-real-ip", b"10.10.10.10"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
ip = _get_ip(scope)
assert ip == "10.10.10.10"
def test_get_headers():
headers = [
(b"x-real-ip", b"10.10.10.10"),
(b"some_header", b"123"),
(b"some_header", b"abc"),
]
scope = {
"client": ("127.0.0.1", 60457),
"headers": headers,
}
headers = _get_headers(scope)
assert headers == {
"x-real-ip": "10.10.10.10",
"some_header": "123, abc",
}
@pytest.mark.asyncio
@pytest.mark.parametrize(
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
[
(
"/message/123456",
"endpoint",
"/message/123456",
"url",
),
(
"/message/123456",
"url",
"/message/123456",
"url",
),
],
)
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_transaction_name(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
asgi3_app,
capture_envelopes,
capture_items,
span_streaming,
):
"""
Tests that the transaction name is something meaningful.
"""
sentry_init(
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
else:
envelopes = capture_envelopes()
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
async with TestClient(app) as client:
await client.get(request_url)
if span_streaming:
sentry_sdk.flush()
assert len(items) == 1
span = items[0].payload
assert span["name"] == expected_transaction_name
assert span["attributes"]["sentry.span.source"] == expected_transaction_source
else:
(transaction_envelope,) = envelopes
transaction_event = transaction_envelope.get_transaction_event()
assert transaction_event["transaction"] == expected_transaction_name
assert (
transaction_event["transaction_info"]["source"]
== expected_transaction_source
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"request_url, transaction_style,expected_transaction_name,expected_transaction_source",
[
(
"/message/123456",
"endpoint",
"/message/123456",
"url",
),
(
"/message/123456",
"url",
"/message/123456",
"url",
),
],
)
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_transaction_name_in_traces_sampler(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
asgi3_app,
span_streaming,
):
"""
Tests that a custom traces_sampler has a meaningful transaction name.
In this case the URL or endpoint, because we do not have the route yet.
"""
def dummy_traces_sampler(sampling_context):
if span_streaming:
assert sampling_context["span_context"]["name"] == expected_transaction_name
assert (
sampling_context["span_context"]["attributes"]["sentry.span.source"]
== expected_transaction_source
)
else:
assert (
sampling_context["transaction_context"]["name"]
== expected_transaction_name
)
assert (
sampling_context["transaction_context"]["source"]
== expected_transaction_source
)
sentry_init(
traces_sampler=dummy_traces_sampler,
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
async with TestClient(app) as client:
await client.get(request_url)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"span_streaming",
[True, False],
)
async def test_custom_transaction_name(
sentry_init,
asgi3_custom_transaction_app,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
app = SentryAsgiMiddleware(asgi3_custom_transaction_app)
async with TestClient(app) as client:
if span_streaming:
items = capture_items("span")
else:
events = capture_events()
await client.get("/test")
sentry_sdk.flush()
if span_streaming:
assert len(items) == 1
span = items[0].payload
assert span["is_segment"] is True
assert span["name"] == "foobar"
assert span["attributes"]["sentry.span.source"] == "custom"
else:
(transaction_event,) = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "foobar"
assert transaction_event["transaction_info"] == {"source": "custom"}
@pytest.mark.asyncio
@pytest.mark.parametrize("send_default_pii", [True, False])
async def test_user_ip_address_on_all_spans(
sentry_init,
capture_items,
send_default_pii,
):
async def app(scope, receive, send):
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
with sentry_sdk.traces.start_span(name="child-span"):
pass
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain"]],
}
)
await send({"type": "http.response.body", "body": b"Hello, world!"})
sentry_init(
send_default_pii=send_default_pii,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
sentry_app = SentryAsgiMiddleware(app)
async def wrapped_app(scope, receive, send):
scope["client"] = ("127.0.0.1", 0)
await sentry_app(scope, receive, send)
async with TestClient(wrapped_app) as client:
items = capture_items("span")
await client.get("/some_url")
sentry_sdk.flush()
child_span, server_span = [item.payload for item in items]
if send_default_pii:
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
else:
assert "user.ip_address" not in server_span["attributes"]
assert "user.ip_address" not in child_span["attributes"]
sentry-python-2.64.0/tests/integrations/asyncio/ 0000775 0000000 0000000 00000000000 15220673775 0022001 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asyncio/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0024100 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asyncio/test_asyncio.py 0000664 0000000 0000000 00000065633 15220673775 0025074 0 ustar 00root root 0000000 0000000 import asyncio
import inspect
import sys
from unittest.mock import MagicMock, Mock, patch
if sys.version_info >= (3, 8):
from unittest.mock import AsyncMock
import pytest
import sentry_sdk
from sentry_sdk.consts import OP
from sentry_sdk.integrations.asyncio import (
AsyncioIntegration,
enable_asyncio_integration,
patch_asyncio,
)
try:
from contextvars import Context, ContextVar
except ImportError:
pass # All tests will be skipped with incompatible versions
minimum_python_38 = pytest.mark.skipif(
sys.version_info < (3, 8), reason="Asyncio tests need Python >= 3.8"
)
minimum_python_39 = pytest.mark.skipif(
sys.version_info < (3, 9), reason="Test requires Python >= 3.9"
)
minimum_python_311 = pytest.mark.skipif(
sys.version_info < (3, 11),
reason="Asyncio task context parameter was introduced in Python 3.11",
)
async def foo():
await asyncio.sleep(0.01)
async def bar():
await asyncio.sleep(0.01)
async def boom():
1 / 0
def get_sentry_task_factory(mock_get_running_loop):
"""
Patches (mocked) asyncio and gets the sentry_task_factory.
"""
mock_loop = mock_get_running_loop.return_value
patch_asyncio()
patched_factory = mock_loop.set_task_factory.call_args[0][0]
return patched_factory
@minimum_python_38
@pytest.mark.asyncio(loop_scope="module")
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_create_task(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[
AsyncioIntegration(),
],
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(
name="not so important", attributes={"sentry.op": "root"}
):
foo_task = asyncio.create_task(foo())
bar_task = asyncio.create_task(bar())
if hasattr(foo_task.get_coro(), "__name__"):
assert foo_task.get_coro().__name__ == "foo"
if hasattr(bar_task.get_coro(), "__name__"):
assert bar_task.get_coro().__name__ == "bar"
tasks = [foo_task, bar_task]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction_for_create_task"):
with sentry_sdk.start_span(op="root", name="not so important"):
foo_task = asyncio.create_task(foo())
bar_task = asyncio.create_task(bar())
if hasattr(foo_task.get_coro(), "__name__"):
assert foo_task.get_coro().__name__ == "foo"
if hasattr(bar_task.get_coro(), "__name__"):
assert bar_task.get_coro().__name__ == "bar"
tasks = [foo_task, bar_task]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
if span_streaming:
segment = items.pop().payload
assert segment["is_segment"] is True
assert segment["name"] == "not so important"
assert segment["attributes"]["sentry.op"] == "root"
spans = [item.payload for item in items]
assert len(spans) == 2
assert spans[0]["attributes"]["sentry.op"] == OP.FUNCTION
assert spans[0]["name"] == "foo"
assert spans[0]["parent_span_id"] == segment["span_id"]
assert spans[1]["attributes"]["sentry.op"] == OP.FUNCTION
assert spans[1]["name"] == "bar"
assert spans[1]["parent_span_id"] == segment["span_id"]
else:
(transaction_event,) = events
assert transaction_event["spans"][0]["op"] == "root"
assert transaction_event["spans"][0]["description"] == "not so important"
assert transaction_event["spans"][1]["op"] == OP.FUNCTION
assert transaction_event["spans"][1]["description"] == "foo"
assert (
transaction_event["spans"][1]["parent_span_id"]
== transaction_event["spans"][0]["span_id"]
)
assert transaction_event["spans"][2]["op"] == OP.FUNCTION
assert transaction_event["spans"][2]["description"] == "bar"
assert (
transaction_event["spans"][2]["parent_span_id"]
== transaction_event["spans"][0]["span_id"]
)
@minimum_python_38
@pytest.mark.asyncio(loop_scope="module")
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_gather(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[
AsyncioIntegration(),
],
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(
name="not so important", attributes={"sentry.op": "root"}
):
await asyncio.gather(foo(), bar(), return_exceptions=True)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction_for_gather"):
with sentry_sdk.start_span(op="root", name="not so important"):
await asyncio.gather(foo(), bar(), return_exceptions=True)
sentry_sdk.flush()
if span_streaming:
segment = items.pop().payload
assert segment["is_segment"] is True
assert segment["name"] == "not so important"
assert segment["attributes"]["sentry.op"] == "root"
spans = [item.payload for item in items]
assert len(spans) == 2
assert spans[0]["attributes"]["sentry.op"] == OP.FUNCTION
assert spans[0]["name"] == "foo"
assert spans[0]["parent_span_id"] == segment["span_id"]
assert spans[1]["attributes"]["sentry.op"] == OP.FUNCTION
assert spans[1]["name"] == "bar"
assert spans[1]["parent_span_id"] == segment["span_id"]
else:
(transaction_event,) = events
assert transaction_event["spans"][0]["op"] == "root"
assert transaction_event["spans"][0]["description"] == "not so important"
assert transaction_event["spans"][1]["op"] == OP.FUNCTION
assert transaction_event["spans"][1]["description"] == "foo"
assert (
transaction_event["spans"][1]["parent_span_id"]
== transaction_event["spans"][0]["span_id"]
)
assert transaction_event["spans"][2]["op"] == OP.FUNCTION
assert transaction_event["spans"][2]["description"] == "bar"
assert (
transaction_event["spans"][2]["parent_span_id"]
== transaction_event["spans"][0]["span_id"]
)
@minimum_python_38
@pytest.mark.asyncio(loop_scope="module")
async def test_exception(
sentry_init,
capture_events,
):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[
AsyncioIntegration(),
],
)
events = capture_events()
with sentry_sdk.start_transaction(name="test_exception"):
with sentry_sdk.start_span(op="root", name="not so important"):
tasks = [asyncio.create_task(boom()), asyncio.create_task(bar())]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
(error_event, _) = events
assert error_event["transaction"] == "test_exception"
assert error_event["contexts"]["trace"]["op"] == "function"
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert error_event["exception"]["values"][0]["value"] == "division by zero"
assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "asyncio"
@minimum_python_38
@pytest.mark.asyncio(loop_scope="module")
async def test_task_result(sentry_init):
sentry_init(
integrations=[
AsyncioIntegration(),
],
)
async def add(a, b):
return a + b
result = await asyncio.create_task(add(1, 2))
assert result == 3, result
@minimum_python_311
@pytest.mark.asyncio(loop_scope="module")
async def test_task_with_context(sentry_init):
"""
Integration test to ensure working context parameter in Python 3.11+
"""
sentry_init(
integrations=[
AsyncioIntegration(),
],
)
var = ContextVar("var")
var.set("original value")
async def change_value():
var.set("changed value")
async def retrieve_value():
return var.get()
# Create a context and run both tasks within the context
ctx = Context()
async with asyncio.TaskGroup() as tg:
tg.create_task(change_value(), context=ctx)
retrieve_task = tg.create_task(retrieve_value(), context=ctx)
assert retrieve_task.result() == "changed value"
@minimum_python_38
@patch("asyncio.get_running_loop")
def test_patch_asyncio(mock_get_running_loop):
"""
Test that the patch_asyncio function will patch the task factory.
"""
mock_loop = mock_get_running_loop.return_value
mock_loop.get_task_factory.return_value._is_sentry_task_factory = False
patch_asyncio()
assert mock_loop.set_task_factory.called
set_task_factory_args, _ = mock_loop.set_task_factory.call_args
assert len(set_task_factory_args) == 1
sentry_task_factory, *_ = set_task_factory_args
assert callable(sentry_task_factory)
@minimum_python_38
@patch("asyncio.get_running_loop")
@patch("sentry_sdk.integrations.asyncio.Task")
def test_sentry_task_factory_no_factory(MockTask, mock_get_running_loop): # noqa: N803
mock_loop = mock_get_running_loop.return_value
mock_coro = MagicMock()
# Set the original task factory to None
mock_loop.get_task_factory.return_value = None
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
# The call we are testing
ret_val = sentry_task_factory(mock_loop, mock_coro)
assert MockTask.called
assert ret_val == MockTask.return_value
task_args, task_kwargs = MockTask.call_args
assert len(task_args) == 1
coro_param, *_ = task_args
assert inspect.iscoroutine(coro_param)
assert "loop" in task_kwargs
assert task_kwargs["loop"] == mock_loop
@minimum_python_38
@patch("asyncio.get_running_loop")
def test_sentry_task_factory_with_factory(mock_get_running_loop):
mock_loop = mock_get_running_loop.return_value
mock_coro = MagicMock()
# The original task factory will be mocked out here, let's retrieve the value for later
orig_task_factory = mock_loop.get_task_factory.return_value
orig_task_factory._is_sentry_task_factory = False
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
# The call we are testing
ret_val = sentry_task_factory(mock_loop, mock_coro)
assert orig_task_factory.called
assert ret_val == orig_task_factory.return_value
task_factory_args, _ = orig_task_factory.call_args
assert len(task_factory_args) == 2
loop_arg, coro_arg = task_factory_args
assert loop_arg == mock_loop
assert inspect.iscoroutine(coro_arg)
@minimum_python_311
@patch("asyncio.get_running_loop")
@patch("sentry_sdk.integrations.asyncio.Task")
def test_sentry_task_factory_context_no_factory(
MockTask,
mock_get_running_loop, # noqa: N803
):
mock_loop = mock_get_running_loop.return_value
mock_coro = MagicMock()
mock_context = MagicMock()
# Set the original task factory to None
mock_loop.get_task_factory.return_value = None
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
# The call we are testing
ret_val = sentry_task_factory(mock_loop, mock_coro, context=mock_context)
assert MockTask.called
assert ret_val == MockTask.return_value
task_args, task_kwargs = MockTask.call_args
assert len(task_args) == 1
coro_param, *_ = task_args
assert inspect.iscoroutine(coro_param)
assert "loop" in task_kwargs
assert task_kwargs["loop"] == mock_loop
assert "context" in task_kwargs
assert task_kwargs["context"] == mock_context
@minimum_python_311
@patch("asyncio.get_running_loop")
def test_sentry_task_factory_context_with_factory(mock_get_running_loop):
mock_loop = mock_get_running_loop.return_value
mock_coro = MagicMock()
mock_context = MagicMock()
# The original task factory will be mocked out here, let's retrieve the value for later
orig_task_factory = mock_loop.get_task_factory.return_value
orig_task_factory._is_sentry_task_factory = False
# Retieve sentry task factory (since it is an inner function within patch_asyncio)
sentry_task_factory = get_sentry_task_factory(mock_get_running_loop)
# The call we are testing
ret_val = sentry_task_factory(mock_loop, mock_coro, context=mock_context)
assert orig_task_factory.called
assert ret_val == orig_task_factory.return_value
task_factory_args, task_factory_kwargs = orig_task_factory.call_args
assert len(task_factory_args) == 2
loop_arg, coro_arg = task_factory_args
assert loop_arg == mock_loop
assert inspect.iscoroutine(coro_arg)
assert "context" in task_factory_kwargs
assert task_factory_kwargs["context"] == mock_context
@minimum_python_38
@pytest.mark.asyncio(loop_scope="module")
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_span_origin(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[AsyncioIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="something"):
tasks = [
asyncio.create_task(foo()),
]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="something"):
tasks = [
asyncio.create_task(foo()),
]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
if span_streaming:
segment = items.pop().payload
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.origin"] == "manual"
spans = [item.payload for item in items]
assert len(spans) == 1
assert spans[0]["attributes"]["sentry.origin"] == "auto.function.asyncio"
else:
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.function.asyncio"
@minimum_python_38
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_task_spans_false(
sentry_init,
capture_events,
capture_items,
uninstall_integration,
span_streaming,
):
uninstall_integration("asyncio")
sentry_init(
traces_sample_rate=1.0,
integrations=[
AsyncioIntegration(task_spans=False),
],
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_no_spans"):
tasks = [asyncio.create_task(foo()), asyncio.create_task(bar())]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_no_spans"):
tasks = [asyncio.create_task(foo()), asyncio.create_task(bar())]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
if span_streaming:
segment = items.pop().payload
assert segment["is_segment"] is True
assert segment["name"] == "test_no_spans"
spans = [item.payload for item in items]
assert len(spans) == 0
else:
(transaction_event,) = events
assert not transaction_event["spans"]
@minimum_python_38
@pytest.mark.asyncio
async def test_enable_asyncio_integration_with_task_spans_false(
sentry_init,
capture_events,
uninstall_integration,
):
"""
Test that enable_asyncio_integration() helper works with task_spans=False.
"""
uninstall_integration("asyncio")
sentry_init(traces_sample_rate=1.0)
assert "asyncio" not in sentry_sdk.get_client().integrations
enable_asyncio_integration(task_spans=False)
assert "asyncio" in sentry_sdk.get_client().integrations
assert sentry_sdk.get_client().integrations["asyncio"].task_spans is False
events = capture_events()
with sentry_sdk.start_transaction(name="test"):
await asyncio.create_task(foo())
sentry_sdk.flush()
(transaction,) = events
assert not transaction["spans"]
@minimum_python_38
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_delayed_enable_integration(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
assert "asyncio" not in sentry_sdk.get_client().integrations
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test"):
await asyncio.create_task(foo())
sentry_sdk.flush()
assert len(items) == 1
assert items[0].payload.get("is_segment") is True
items.clear()
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test"):
await asyncio.create_task(foo())
assert len(events) == 1
(transaction,) = events
assert not transaction["spans"]
enable_asyncio_integration()
assert "asyncio" in sentry_sdk.get_client().integrations
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test"):
await asyncio.create_task(foo())
sentry_sdk.flush()
segment = items.pop().payload
assert segment["is_segment"] is True
spans = [item.payload for item in items]
assert len(spans) == 1
assert spans[0]["attributes"]["sentry.origin"] == "auto.function.asyncio"
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test"):
await asyncio.create_task(foo())
assert len(events) == 1
(transaction,) = events
assert transaction["spans"]
assert transaction["spans"][0]["origin"] == "auto.function.asyncio"
@minimum_python_38
@pytest.mark.asyncio
async def test_delayed_enable_integration_with_options(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0)
assert "asyncio" not in sentry_sdk.get_client().integrations
mock_init = MagicMock(return_value=None)
mock_setup_once = MagicMock()
with patch(
"sentry_sdk.integrations.asyncio.AsyncioIntegration.__init__", mock_init
):
with patch(
"sentry_sdk.integrations.asyncio.AsyncioIntegration.setup_once",
mock_setup_once,
):
enable_asyncio_integration("arg", kwarg="kwarg")
assert "asyncio" in sentry_sdk.get_client().integrations
mock_init.assert_called_once_with("arg", kwarg="kwarg")
mock_setup_once.assert_called_once()
@minimum_python_38
@pytest.mark.asyncio
async def test_delayed_enable_enabled_integration(sentry_init, uninstall_integration):
# Ensure asyncio integration is not already installed from previous tests
uninstall_integration("asyncio")
integration = AsyncioIntegration()
sentry_init(integrations=[integration], traces_sample_rate=1.0)
assert "asyncio" in sentry_sdk.get_client().integrations
# Get the task factory after initial setup - it should be Sentry's
loop = asyncio.get_running_loop()
task_factory_before = loop.get_task_factory()
assert getattr(task_factory_before, "_is_sentry_task_factory", False) is True
enable_asyncio_integration()
assert "asyncio" in sentry_sdk.get_client().integrations
# The task factory should be the same (loop not re-patched)
task_factory_after = loop.get_task_factory()
assert task_factory_before is task_factory_after
@minimum_python_38
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_delayed_enable_integration_after_disabling(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
disabled_integrations=[AsyncioIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
assert "asyncio" not in sentry_sdk.get_client().integrations
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test"):
await asyncio.create_task(foo())
sentry_sdk.flush()
assert len(items) == 1
assert items[0].payload.get("is_segment") is True
items.clear()
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test"):
await asyncio.create_task(foo())
assert len(events) == 1
(transaction,) = events
assert not transaction["spans"]
enable_asyncio_integration()
assert "asyncio" in sentry_sdk.get_client().integrations
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test"):
await asyncio.create_task(foo())
sentry_sdk.flush()
segment = items.pop().payload
assert segment["is_segment"] is True
spans = [item.payload for item in items]
assert len(spans) == 1
assert spans[0]["attributes"]["sentry.origin"] == "auto.function.asyncio"
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test"):
await asyncio.create_task(foo())
assert len(events) == 1
(transaction,) = events
assert transaction["spans"]
assert transaction["spans"][0]["origin"] == "auto.function.asyncio"
@minimum_python_39
@pytest.mark.asyncio(loop_scope="module")
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_internal_tasks_not_wrapped(
sentry_init, capture_events, capture_items, span_streaming
):
from sentry_sdk.utils import mark_sentry_task_internal
sentry_init(
integrations=[AsyncioIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
async def user_task():
await asyncio.sleep(0.01)
return "user_result"
async def internal_task():
await asyncio.sleep(0.01)
return "internal_result"
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_streamed_span"):
user_task_obj = asyncio.create_task(user_task())
with mark_sentry_task_internal():
internal_task_obj = asyncio.create_task(internal_task())
user_result = await user_task_obj
internal_result = await internal_task_obj
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction"):
user_task_obj = asyncio.create_task(user_task())
with mark_sentry_task_internal():
internal_task_obj = asyncio.create_task(internal_task())
user_result = await user_task_obj
internal_result = await internal_task_obj
assert user_result == "user_result"
assert internal_result == "internal_result"
sentry_sdk.flush()
if span_streaming:
assert len(items) == 2
segment = items.pop().payload
assert segment["is_segment"] is True
assert segment["name"] == "test_streamed_span"
spans = [item.payload for item in items]
assert len(spans) == 1
assert spans[0]["name"].endswith("user_task")
else:
assert len(events) == 1
transaction = events[0]
user_spans = []
internal_spans = []
for span in transaction.get("spans", []):
if "user_task" in span.get("description", ""):
user_spans.append(span)
elif "internal_task" in span.get("description", ""):
internal_spans.append(span)
assert len(user_spans) > 0, (
f"User task should have been traced. All spans: {[s.get('description') for s in transaction.get('spans', [])]}"
)
assert len(internal_spans) == 0, (
f"Internal task should NOT have been traced. All spans: {[s.get('description') for s in transaction.get('spans', [])]}"
)
@minimum_python_38
def test_loop_close_patching(sentry_init):
sentry_init(integrations=[AsyncioIntegration()])
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
with patch("asyncio.get_running_loop", return_value=loop):
assert not hasattr(loop, "_sentry_flush_patched")
AsyncioIntegration.setup_once()
assert hasattr(loop, "_sentry_flush_patched")
assert loop._sentry_flush_patched is True
finally:
if not loop.is_closed():
loop.close()
@minimum_python_38
def test_loop_close_flushes_async_transport(sentry_init):
from sentry_sdk.transport import ASYNC_TRANSPORT_AVAILABLE, AsyncHttpTransport
if not ASYNC_TRANSPORT_AVAILABLE:
pytest.skip("httpcore[asyncio] not installed")
sentry_init(integrations=[AsyncioIntegration()])
# Save the current event loop to restore it later
try:
original_loop = asyncio.get_event_loop()
except RuntimeError:
original_loop = None
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
with patch("asyncio.get_running_loop", return_value=loop):
AsyncioIntegration.setup_once()
mock_client = Mock()
mock_transport = Mock(spec=AsyncHttpTransport)
mock_client.transport = mock_transport
mock_client.close_async = AsyncMock(return_value=None)
with patch("sentry_sdk.get_client", return_value=mock_client):
loop.close()
mock_client.close_async.assert_called_once()
mock_client.close_async.assert_awaited_once()
finally:
if not loop.is_closed():
loop.close()
if original_loop:
asyncio.set_event_loop(original_loop)
sentry-python-2.64.0/tests/integrations/asyncpg/ 0000775 0000000 0000000 00000000000 15220673775 0022000 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asyncpg/__init__.py 0000664 0000000 0000000 00000000505 15220673775 0024111 0 ustar 00root root 0000000 0000000 import os
import sys
import pytest
pytest.importorskip("asyncpg")
pytest.importorskip("pytest_asyncio")
# Load `asyncpg_helpers` into the module search path to test query source path names relative to module. See
# `test_query_source_with_module_in_search_path`
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
sentry-python-2.64.0/tests/integrations/asyncpg/asyncpg_helpers/ 0000775 0000000 0000000 00000000000 15220673775 0025166 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asyncpg/asyncpg_helpers/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0027265 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/asyncpg/asyncpg_helpers/helpers.py 0000664 0000000 0000000 00000000136 15220673775 0027202 0 ustar 00root root 0000000 0000000 async def execute_query_in_connection(query, connection):
await connection.execute(query)
sentry-python-2.64.0/tests/integrations/asyncpg/test_asyncpg.py 0000664 0000000 0000000 00000141440 15220673775 0025061 0 ustar 00root root 0000000 0000000 """
Tests need pytest-asyncio installed.
Tests need a local postgresql instance running, this can best be done using
```sh
docker run --rm --name some-postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=sentry -d -p 5432:5432 postgres
```
The tests use the following credentials to establish a database connection.
"""
import datetime
import os
from contextlib import contextmanager
from unittest import mock
import asyncpg
import pytest
import pytest_asyncio
from asyncpg import Connection, connect
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations.asyncpg import AsyncPGIntegration
from sentry_sdk.tracing_utils import record_sql_queries
from tests.conftest import ApproxDict
PG_HOST = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost")
PG_PORT = int(os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"))
PG_USER = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_USER", "postgres")
PG_PASSWORD = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_PASSWORD", "sentry")
PG_NAME_BASE = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_NAME", "postgres")
def _get_db_name():
pid = os.getpid()
return f"{PG_NAME_BASE}_{pid}"
PG_NAME = _get_db_name()
PG_CONNECTION_URI = "postgresql://{}:{}@{}/{}".format(
PG_USER, PG_PASSWORD, PG_HOST, PG_NAME
)
CRUMBS_CONNECT = {
"category": "query",
"data": ApproxDict(
{
"db.name": PG_NAME,
"db.system": "postgresql",
"db.user": PG_USER,
"db.driver.name": "asyncpg",
"server.address": PG_HOST,
"server.port": PG_PORT,
}
),
"message": "connect",
"type": "default",
}
CRUMBS_CONNECT_STREAMING = {
"category": "query",
"data": ApproxDict(
{
"sentry.op": "db",
"sentry.origin": "auto.db.asyncpg",
"db.system.name": "postgresql",
"db.namespace": PG_NAME,
"db.user": PG_USER,
"db.driver.name": "asyncpg",
"server.address": PG_HOST,
"server.port": PG_PORT,
}
),
"message": "connect",
"type": "default",
}
@pytest_asyncio.fixture(autouse=True)
async def _clean_pg():
# Create the test database if it doesn't exist
default_conn = await connect(
"postgresql://{}:{}@{}".format(PG_USER, PG_PASSWORD, PG_HOST)
)
try:
# Check if database exists, create if not
result = await default_conn.fetchval(
"SELECT 1 FROM pg_database WHERE datname = $1", PG_NAME
)
if not result:
await default_conn.execute(f'CREATE DATABASE "{PG_NAME}"')
finally:
await default_conn.close()
# Now connect to our test database and set up the table
conn = await connect(PG_CONNECTION_URI)
await conn.execute("DROP TABLE IF EXISTS users")
await conn.execute(
"""
CREATE TABLE users(
id serial PRIMARY KEY,
name text,
password text,
dob date
)
"""
)
await conn.close()
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_connect(
sentry_init, capture_events, capture_items, span_streaming
) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("event")
else:
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.close()
capture_message("hi")
if span_streaming:
event = items[0].payload
else:
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
expected_crumbs_connect = (
CRUMBS_CONNECT_STREAMING if span_streaming else CRUMBS_CONNECT
)
assert event["breadcrumbs"]["values"] == [expected_crumbs_connect]
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_execute(
sentry_init, capture_events, capture_items, span_streaming
) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("event")
else:
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')",
)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"Bob",
"secret_pw",
datetime.date(1984, 3, 1),
)
row = await conn.fetchrow("SELECT * FROM users WHERE name = $1", "Bob")
assert row == (2, "Bob", "secret_pw", datetime.date(1984, 3, 1))
row = await conn.fetchrow("SELECT * FROM users WHERE name = 'Bob'")
assert row == (2, "Bob", "secret_pw", datetime.date(1984, 3, 1))
await conn.close()
capture_message("hi")
if span_streaming:
event = items[0].payload
else:
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
expected_crumbs_connect = (
CRUMBS_CONNECT_STREAMING if span_streaming else CRUMBS_CONNECT
)
assert event["breadcrumbs"]["values"] == [
expected_crumbs_connect,
{
"category": "query",
"data": {},
"message": "INSERT INTO users(name, password, dob) VALUES ('Alice', 'pw', '1990-12-25')",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT * FROM users WHERE name = $1",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT * FROM users WHERE name = 'Bob'",
"type": "default",
},
]
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_execute_many(
sentry_init, capture_events, capture_items, span_streaming
) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("event")
else:
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
await conn.close()
capture_message("hi")
if span_streaming:
event = items[0].payload
else:
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
expected_crumbs_connect = (
CRUMBS_CONNECT_STREAMING if span_streaming else CRUMBS_CONNECT
)
assert event["breadcrumbs"]["values"] == [
expected_crumbs_connect,
{
"category": "query",
"data": {"db.executemany": True},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_record_params(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AsyncPGIntegration(record_params=True)],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"Bob",
"secret_pw",
datetime.date(1984, 3, 1),
)
await conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {
"db.params": ["Bob", "secret_pw", "datetime.date(1984, 3, 1)"],
"db.paramstyle": "format",
},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_cursor(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
async with conn.transaction():
# Postgres requires non-scrollable cursors to be created
# and used in a transaction.
async for record in conn.cursor(
"SELECT * FROM users WHERE dob > $1", datetime.date(1970, 1, 1)
):
print(record)
await conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {"db.executemany": True},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
{"category": "query", "data": {}, "message": "BEGIN;", "type": "default"},
{
"category": "query",
"data": {"db.cursor": mock.ANY},
"message": "SELECT * FROM users WHERE dob > $1",
"type": "default",
},
{"category": "query", "data": {}, "message": "COMMIT;", "type": "default"},
]
@pytest.mark.asyncio
async def test_cursor_manual(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
#
async with conn.transaction():
# Postgres requires non-scrollable cursors to be created
# and used in a transaction.
cur = await conn.cursor(
"SELECT * FROM users WHERE dob > $1", datetime.date(1970, 1, 1)
)
record = await cur.fetchrow()
print(record)
while await cur.forward(1):
record = await cur.fetchrow()
print(record)
await conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {"db.executemany": True},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
{"category": "query", "data": {}, "message": "BEGIN;", "type": "default"},
{
"category": "query",
"data": {"db.cursor": mock.ANY},
"message": "SELECT * FROM users WHERE dob > $1",
"type": "default",
},
{
"category": "query",
"data": {"db.cursor": mock.ANY},
"message": "SELECT * FROM users WHERE dob > $1",
"type": "default",
},
{"category": "query", "data": {}, "message": "COMMIT;", "type": "default"},
]
@pytest.mark.asyncio
async def test_prepared_stmt(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
stmt = await conn.prepare("SELECT * FROM users WHERE name = $1")
print(await stmt.fetchval("Bob"))
print(await stmt.fetchval("Alice"))
await conn.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
CRUMBS_CONNECT,
{
"category": "query",
"data": {"db.executemany": True},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT * FROM users WHERE name = $1",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_connection_pool(sentry_init, capture_events) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
pool_size = 2
pool = await asyncpg.create_pool(
PG_CONNECTION_URI, min_size=pool_size, max_size=pool_size
)
async with pool.acquire() as conn:
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"Bob",
"secret_pw",
datetime.date(1984, 3, 1),
)
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT * FROM users WHERE name = $1", "Bob")
assert row == (1, "Bob", "secret_pw", datetime.date(1984, 3, 1))
await pool.close()
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"] == [
# The connection pool opens pool_size connections so we have the crumbs pool_size times
*[CRUMBS_CONNECT] * pool_size,
{
"category": "query",
"data": {},
"message": "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT * FROM users WHERE name = $1",
"type": "default",
},
{
"category": "query",
"data": {},
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
"type": "default",
},
]
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_disabled(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_options = {
"integrations": [AsyncPGIntegration()],
"traces_sample_rate": 1.0,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
"_experiments": {
"trace_lifecycle": "stream" if span_streaming else "static",
},
}
sentry_init(**sentry_options)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
@pytest.mark.parametrize("enable_db_query_source", [None, True])
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_enabled(
sentry_init, capture_events, capture_items, enable_db_query_source, span_streaming
):
sentry_options = {
"integrations": [AsyncPGIntegration()],
"traces_sample_rate": 1.0,
"db_query_source_threshold_ms": 0,
"_experiments": {
"trace_lifecycle": "stream" if span_streaming else "static",
},
}
if enable_db_query_source is not None:
sentry_options["enable_db_query_source"] = enable_db_query_source
sentry_init(**sentry_options)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
lineno_key = "code.line.number" if span_streaming else SPANDATA.CODE_LINENO
filepath_key = "code.file.path" if span_streaming else SPANDATA.CODE_FILEPATH
assert lineno_key in data
assert filepath_key in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FUNCTION in data
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source(sentry_init, capture_events, capture_items, span_streaming):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
lineno_key = "code.line.number" if span_streaming else SPANDATA.CODE_LINENO
filepath_key = "code.file.path" if span_streaming else SPANDATA.CODE_FILEPATH
assert lineno_key in data
assert filepath_key in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(lineno_key)) == int
assert data.get(lineno_key) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.asyncpg.test_asyncpg"
)
assert data.get(filepath_key).endswith("tests/integrations/asyncpg/test_asyncpg.py")
is_relative_path = data.get(filepath_key)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_with_module_in_search_path(
sentry_init, capture_events, capture_items, span_streaming
):
"""
Test that query source is relative to the path of the module it ran in
"""
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
from asyncpg_helpers.helpers import execute_query_in_connection
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await execute_query_in_connection(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
conn,
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await execute_query_in_connection(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
conn,
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
lineno_key = "code.line.number" if span_streaming else SPANDATA.CODE_LINENO
filepath_key = "code.file.path" if span_streaming else SPANDATA.CODE_FILEPATH
assert lineno_key in data
assert filepath_key in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(lineno_key)) == int
assert data.get(lineno_key) > 0
assert data.get(filepath_key) == "asyncpg_helpers/helpers.py"
assert data.get(SPANDATA.CODE_NAMESPACE) == "asyncpg_helpers.helpers"
is_relative_path = data.get(filepath_key)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "execute_query_in_connection"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_no_query_source_if_duration_too_short(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
@contextmanager
def fake_record_sql_queries_streaming(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span._start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
if span_streaming:
span._end_timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
else:
span._timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
yield span
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
with mock.patch(
"sentry_sdk.integrations.asyncpg.record_sql_queries",
fake_record_sql_queries_streaming,
):
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
insert_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert insert_span["name"].startswith("INSERT INTO")
assert connect_span["name"] == "connect"
data = insert_span.get("attributes", {})
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
@contextmanager
def fake_record_sql_queries(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=99999)
yield span
with mock.patch(
"sentry_sdk.integrations.asyncpg.record_sql_queries",
fake_record_sql_queries,
):
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
@pytest.mark.asyncio
async def test_query_source_if_duration_over_threshold(sentry_init, capture_events):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
@contextmanager
def fake_record_sql_queries(*args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
pass
span.start_timestamp = datetime.datetime(2024, 1, 1, microsecond=0)
span.timestamp = datetime.datetime(2024, 1, 1, microsecond=100001)
yield span
with mock.patch(
"sentry_sdk.integrations.asyncpg.record_sql_queries",
fake_record_sql_queries,
):
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
)
await conn.close()
(event,) = events
span = event["spans"][-1]
assert span["description"].startswith("INSERT INTO")
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.asyncpg.test_asyncpg"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/asyncpg/test_asyncpg.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert (
data.get(SPANDATA.CODE_FUNCTION)
== "test_query_source_if_duration_over_threshold"
)
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_span_origin(sentry_init, capture_events, capture_items, span_streaming):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute("SELECT 1")
await conn.fetchrow("SELECT 2")
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 4
connect_span = spans[0]
select1_span = spans[1]
select2_span = spans[2]
segment = spans[3]
assert segment["name"] == "test_segment"
assert connect_span["name"] == "connect"
assert select1_span["name"] == "SELECT 1"
assert select2_span["name"] == "SELECT 2"
assert segment["attributes"]["sentry.origin"] == "manual"
assert connect_span["attributes"]["sentry.origin"] == "auto.db.asyncpg"
assert select1_span["attributes"]["sentry.origin"] == "auto.db.asyncpg"
assert select2_span["attributes"]["sentry.origin"] == "auto.db.asyncpg"
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute("SELECT 1")
await conn.fetchrow("SELECT 2")
await conn.close()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.asyncpg"
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_multiline_query_description_normalized(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"""
SELECT
id,
name
FROM
users
WHERE
name = 'Alice'
"""
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
select_span = spans[1]
segment = spans[2]
assert segment["name"] == "test_segment"
assert connect_span["name"] == "connect"
assert select_span["name"] == "SELECT id, name FROM users WHERE name = 'Alice'"
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"""
SELECT
id,
name
FROM
users
WHERE
name = 'Alice'
"""
)
await conn.close()
(event,) = events
spans = [
s
for s in event["spans"]
if s["op"] == "db" and "SELECT" in s.get("description", "")
]
assert len(spans) == 1
assert (
spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'"
)
@pytest.mark.asyncio
async def test_before_send_transaction_sees_normalized_description(
sentry_init, capture_events
):
def before_send_transaction(event, hint):
for span in event.get("spans", []):
desc = span.get("description", "")
if "SELECT id, name FROM users" in desc:
span["description"] = "filtered"
return event
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
before_send_transaction=before_send_transaction,
)
events = capture_events()
with start_transaction(name="test_transaction"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"""
SELECT
id,
name
FROM
users
"""
)
await conn.close()
(event,) = events
spans = [
s
for s in event["spans"]
if s["op"] == "db" and "filtered" in s.get("description", "")
]
assert len(spans) == 1
assert spans[0]["description"] == "filtered"
def _assert_query_source(span, span_streaming, expected_function):
if span_streaming:
data = span.get("attributes", {})
lineno_key = "code.line.number"
filepath_key = "code.file.path"
else:
data = span.get("data", {})
lineno_key = SPANDATA.CODE_LINENO
filepath_key = SPANDATA.CODE_FILEPATH
assert lineno_key in data
assert filepath_key in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(lineno_key)) == int
assert data.get(lineno_key) > 0
assert data[SPANDATA.CODE_NAMESPACE] == "tests.integrations.asyncpg.test_asyncpg"
assert data.get(filepath_key).endswith("tests/integrations/asyncpg/test_asyncpg.py")
assert data.get(filepath_key)[0] != os.sep
assert data[SPANDATA.CODE_FUNCTION] == expected_function
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_execute(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"Alice",
"pw",
datetime.date(1990, 12, 25),
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
query_span = spans[1]
segment = spans[2]
assert connect_span["name"] == "connect"
assert query_span["name"].startswith("INSERT INTO")
assert segment["name"] == "test_segment"
assert segment["is_segment"] is True
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.execute(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
"Alice",
"pw",
datetime.date(1990, 12, 25),
)
await conn.close()
(event,) = events
spans = event["spans"]
assert len(spans) == 2
assert spans[0]["description"] == "connect"
assert spans[1]["description"].startswith("INSERT INTO")
query_span = spans[1]
_assert_query_source(query_span, span_streaming, "test_query_source_execute")
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_executemany(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[("Bob", "secret_pw", datetime.date(1984, 3, 1))],
)
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
query_span = spans[1]
segment = spans[2]
assert connect_span["name"] == "connect"
assert query_span["name"].startswith("INSERT INTO")
assert segment["name"] == "test_segment"
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[("Bob", "secret_pw", datetime.date(1984, 3, 1))],
)
await conn.close()
(event,) = events
spans = event["spans"]
assert len(spans) == 2
assert spans[0]["description"] == "connect"
assert spans[1]["description"].startswith("INSERT INTO")
query_span = spans[1]
_assert_query_source(query_span, span_streaming, "test_query_source_executemany")
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_query_source_prepare(
sentry_init, capture_events, capture_items, span_streaming
):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.prepare("SELECT * FROM users WHERE name = $1")
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
connect_span = spans[0]
query_span = spans[1]
segment = spans[2]
assert connect_span["name"] == "connect"
assert query_span["name"] == "SELECT * FROM users WHERE name = $1"
assert segment["name"] == "test_segment"
assert (
query_span["attributes"][SPANDATA.DB_QUERY_TEXT]
== "SELECT * FROM users WHERE name = $1"
)
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.prepare("SELECT * FROM users WHERE name = $1")
await conn.close()
(event,) = events
spans = event["spans"]
assert len(spans) == 2
assert spans[0]["description"] == "connect"
assert spans[1]["description"] == "SELECT * FROM users WHERE name = $1"
query_span = spans[1]
_assert_query_source(query_span, span_streaming, "test_query_source_prepare")
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_cursor_iteration_creates_db_cursor_iter_spans(
sentry_init, capture_events, capture_items, span_streaming
) -> None:
"""
Regression test for https://github.com/getsentry/sentry-python/issues/6576
When iterating a server-side cursor with a small prefetch, asyncpg fetches
rows in batches. Each batch triggers BaseCursor._bind_exec (on first query) and
BaseCursor._exec (second query onwards) through CursorIterator.__anext__, which creates a
span with the same query description. The resulting burst of identical spans
causes Sentry's N+1 query detector to raise a false positive.
To mitigate, we set the "op"/"sentry.op" to `db.cursor.iter` instead of `db`
so that the sentry backend can exclude these spans from n+1 detection.
"""
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
_experiments={
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[(f"user-{i}", "pw", datetime.date(1990, 1, 1)) for i in range(20)],
)
async with conn.transaction():
async for _record in conn.cursor("SELECT * FROM users", prefetch=5):
pass
await conn.close()
sentry_sdk.flush()
cursor_iter_spans = [
item.payload
for item in items
if item.payload.get("name") == "SELECT * FROM users"
]
assert len(cursor_iter_spans) == 5
for span in cursor_iter_spans:
assert span["attributes"]["sentry.op"] == OP.DB_CURSOR_ITERATOR
assert span["attributes"][SPANDATA.DB_QUERY_TEXT] == "SELECT * FROM users"
else:
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[(f"user-{i}", "pw", datetime.date(1990, 1, 1)) for i in range(20)],
)
async with conn.transaction():
async for _record in conn.cursor("SELECT * FROM users", prefetch=5):
pass
await conn.close()
(event,) = events
cursor_iter_spans = [
s for s in event["spans"] if s.get("description") == "SELECT * FROM users"
]
assert len(cursor_iter_spans) == 5
for span in cursor_iter_spans:
assert span["op"] == OP.DB_CURSOR_ITERATOR
@pytest.mark.asyncio
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_cursor_fetch_methods_create_spans(
sentry_init, capture_events, capture_items, span_streaming
) -> None:
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items()
with sentry_sdk.traces.start_span(name="test_segment"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
async with conn.transaction():
cur = await conn.cursor(
"SELECT * FROM users WHERE dob > $1", datetime.date(1970, 1, 1)
)
# These exercise the `_exec` patch
await cur.fetchrow()
await cur.fetchrow()
await conn.close()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 7
connect_span = spans[0]
executemany_span = spans[1]
begin_span = spans[2]
fetchrow_span_1 = spans[3]
fetchrow_span_2 = spans[4]
commit_span = spans[5]
_segment_span = spans[6]
assert connect_span["name"] == "connect"
assert (
executemany_span["name"]
== "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)"
)
assert begin_span["name"] == "BEGIN;"
assert fetchrow_span_1["name"] == "SELECT * FROM users WHERE dob > $1"
assert fetchrow_span_2["name"] == "SELECT * FROM users WHERE dob > $1"
assert commit_span["name"] == "COMMIT;"
assert (
fetchrow_span_1["attributes"][SPANDATA.DB_QUERY_TEXT]
== "SELECT * FROM users WHERE dob > $1"
)
assert (
fetchrow_span_2["attributes"][SPANDATA.DB_QUERY_TEXT]
== "SELECT * FROM users WHERE dob > $1"
)
for span in (fetchrow_span_1, fetchrow_span_2):
assert span["attributes"][SPANDATA.DB_SYSTEM_NAME] == "postgresql"
assert span["attributes"][SPANDATA.DB_DRIVER_NAME] == "asyncpg"
assert span["attributes"]["sentry.op"] == OP.DB_CURSOR_FETCH
assert span["attributes"]["sentry.origin"] == "auto.db.asyncpg"
else:
events = capture_events()
with start_transaction(name="test_transaction"):
conn: Connection = await connect(PG_CONNECTION_URI)
await conn.executemany(
"INSERT INTO users(name, password, dob) VALUES($1, $2, $3)",
[
("Bob", "secret_pw", datetime.date(1984, 3, 1)),
("Alice", "pw", datetime.date(1990, 12, 25)),
],
)
async with conn.transaction():
cur = await conn.cursor(
"SELECT * FROM users WHERE dob > $1", datetime.date(1970, 1, 1)
)
# These exercise the `_exec` patch
await cur.fetchrow()
await cur.fetchrow()
await conn.close()
(event,) = events
assert len(event["spans"]) == 6
connect_span = event["spans"][0]
executemany_span = event["spans"][1]
begin_span = event["spans"][2]
fetchrow_span_1 = event["spans"][3]
fetchrow_span_2 = event["spans"][4]
commit_span = event["spans"][5]
assert connect_span["description"] == "connect"
assert (
executemany_span["description"]
== "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)"
)
assert begin_span["description"] == "BEGIN;"
assert fetchrow_span_1["description"] == "SELECT * FROM users WHERE dob > $1"
assert fetchrow_span_2["description"] == "SELECT * FROM users WHERE dob > $1"
assert commit_span["description"] == "COMMIT;"
for span in (fetchrow_span_1, fetchrow_span_2):
assert span["data"]["db.cursor"] is not None
assert span["data"]["db.system"] == "postgresql"
assert span["data"]["db.driver.name"] == "asyncpg"
assert span["op"] == OP.DB_CURSOR_FETCH
assert span["origin"] == "auto.db.asyncpg"
_assert_query_source(
span,
span_streaming,
"test_cursor_fetch_methods_create_spans",
)
sentry-python-2.64.0/tests/integrations/aws_lambda/ 0000775 0000000 0000000 00000000000 15220673775 0022426 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/__init__.py 0000664 0000000 0000000 00000000152 15220673775 0024535 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("boto3")
pytest.importorskip("fastapi")
pytest.importorskip("uvicorn")
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/ 0000775 0000000 0000000 00000000000 15220673775 0025736 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/BasicException/ 0000775 0000000 0000000 00000000000 15220673775 0030636 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/BasicException/index.py 0000664 0000000 0000000 00000000147 15220673775 0032321 0 ustar 00root root 0000000 0000000 def handler(event, context):
raise RuntimeError("Oh!")
return {
"event": event,
}
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/BasicOk/ 0000775 0000000 0000000 00000000000 15220673775 0027251 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/BasicOk/index.py 0000664 0000000 0000000 00000000110 15220673775 0030722 0 ustar 00root root 0000000 0000000 def handler(event, context):
return {
"event": event,
}
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/InitError/ 0000775 0000000 0000000 00000000000 15220673775 0027653 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/InitError/index.py 0000664 0000000 0000000 00000000133 15220673775 0031331 0 ustar 00root root 0000000 0000000 # We have no handler() here and try to call a non-existing function.
func() # noqa: F821
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/TimeoutError/ 0000775 0000000 0000000 00000000000 15220673775 0030376 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py 0000664 0000000 0000000 00000000151 15220673775 0032054 0 ustar 00root root 0000000 0000000 import time
def handler(event, context):
time.sleep(15)
return {
"event": event,
}
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/ 0000775 0000000 0000000 00000000000 15220673775 0031443 5 ustar 00root root 0000000 0000000 BasicOkSpanStreaming/ 0000775 0000000 0000000 00000000000 15220673775 0035373 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0037366 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000515 15220673775 0037055 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration()],
_experiments={"trace_lifecycle": "stream"},
)
def handler(event, context):
return {"event": event}
BasicOkSpanStreamingPii/ 0000775 0000000 0000000 00000000000 15220673775 0036035 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0040030 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000550 15220673775 0037516 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[AwsLambdaIntegration()],
_experiments={"trace_lifecycle": "stream"},
)
def handler(event, context):
return {"event": event}
RaiseErrorPerformanceDisabled/ 0000775 0000000 0000000 00000000000 15220673775 0037253 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0041246 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000514 15220673775 0040734 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceDisabled import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=None, # this is the default, just added for clarity
integrations=[AwsLambdaIntegration()],
)
def handler(event, context):
raise Exception("Oh!")
RaiseErrorPerformanceEnabled/ 0000775 0000000 0000000 00000000000 15220673775 0037076 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0041071 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000434 15220673775 0040560 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorPerformanceEnabled import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration()],
)
def handler(event, context):
raise Exception("Oh!")
RaiseErrorSpanStreaming/ 0000775 0000000 0000000 00000000000 15220673775 0036135 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0040130 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000514 15220673775 0037616 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration()],
_experiments={"trace_lifecycle": "stream"},
)
def handler(event, context):
raise Exception("Oh!")
TimeoutErrorScopeModified/ 0000775 0000000 0000000 00000000000 15220673775 0036457 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk .gitignore 0000664 0000000 0000000 00000000535 15220673775 0040452 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000000624 15220673775 0040142 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified import os
import time
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
integrations=[AwsLambdaIntegration(timeout_warning=True)],
)
def handler(event, context):
sentry_sdk.set_tag("custom_tag", "custom_value")
time.sleep(15)
return {
"event": event,
}
sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler/0000775 0000000 0000000 00000000000 15220673775 0034210 5 ustar 00root root 0000000 0000000 .gitignore 0000664 0000000 0000000 00000000535 15220673775 0036124 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler # Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies
# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry.
# Ignore everything
*
# But not index.py
!index.py
# And not .gitignore itself
!.gitignore
index.py 0000664 0000000 0000000 00000002551 15220673775 0035615 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TracesSampler import json
import os
import sentry_sdk
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
# Global variables to store sampling context for verification
sampling_context_data = {
"aws_event_present": False,
"aws_context_present": False,
"event_data": None,
}
def trace_sampler(sampling_context):
# Store the sampling context for verification
global sampling_context_data
# Check if aws_event and aws_context are in the sampling_context
if "aws_event" in sampling_context:
sampling_context_data["aws_event_present"] = True
sampling_context_data["event_data"] = sampling_context["aws_event"]
if "aws_context" in sampling_context:
sampling_context_data["aws_context_present"] = True
print("Sampling context data:", sampling_context_data)
return 1.0 # Always sample
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
traces_sample_rate=1.0,
traces_sampler=trace_sampler,
integrations=[AwsLambdaIntegration()],
)
def handler(event, context):
# Return the sampling context data for verification
return {
"statusCode": 200,
"body": json.dumps(
{
"message": "Hello from Lambda with embedded Sentry SDK!",
"event": event,
"sampling_context_data": sampling_context_data,
}
),
}
sentry-python-2.64.0/tests/integrations/aws_lambda/test_aws_lambda.py 0000664 0000000 0000000 00000060470 15220673775 0026140 0 ustar 00root root 0000000 0000000 import json
import subprocess
import tempfile
import time
from unittest import mock
import boto3
import docker
import pytest
import yaml
from aws_cdk import App
from .utils import SAM_PORT, LocalLambdaStack, SentryServerForTesting
DOCKER_NETWORK_NAME = "lambda-test-network"
SAM_TEMPLATE_FILE = "sam.template.yaml"
@pytest.fixture(scope="session", autouse=True)
def test_environment():
print("[test_environment fixture] Setting up AWS Lambda test infrastructure")
# Create a Docker network
docker_client = docker.from_env()
docker_client.networks.prune()
docker_client.networks.create(DOCKER_NETWORK_NAME, driver="bridge")
# Start Sentry server
server = SentryServerForTesting()
server.start()
time.sleep(1) # Give it a moment to start up
# Create local AWS SAM stack
app = App()
stack = LocalLambdaStack(app, "LocalLambdaStack")
# Write SAM template to file
template = app.synth().get_stack_by_name("LocalLambdaStack").template
with open(SAM_TEMPLATE_FILE, "w") as f:
yaml.dump(template, f)
# Write SAM debug log to file
debug_log_file = tempfile.gettempdir() + "/sentry_aws_lambda_tests_sam_debug.log"
debug_log = open(debug_log_file, "w")
print("[test_environment fixture] Writing SAM debug log to: %s" % debug_log_file)
# Start SAM local
process = subprocess.Popen(
[
"sam",
"local",
"start-lambda",
"--debug",
"--template",
SAM_TEMPLATE_FILE,
"--warm-containers",
"EAGER",
"--docker-network",
DOCKER_NETWORK_NAME,
],
stdout=debug_log,
stderr=debug_log,
text=True, # This makes stdout/stderr return strings instead of bytes
)
try:
# Wait for SAM to be ready
LocalLambdaStack.wait_for_stack()
def before_test():
server.clear_envelopes()
yield {
"stack": stack,
"server": server,
"before_test": before_test,
}
finally:
print("[test_environment fixture] Tearing down AWS Lambda test infrastructure")
process.terminate()
process.wait(timeout=10) # Give it time to shut down gracefully
# Force kill if still running
if process.poll() is None:
process.kill()
@pytest.fixture(autouse=True)
def clear_before_test(test_environment):
test_environment["before_test"]()
@pytest.fixture
def lambda_client():
"""
Create a boto3 client configured to use the local AWS SAM instance.
"""
return boto3.client(
"lambda",
endpoint_url=f"http://127.0.0.1:{SAM_PORT}", # noqa: E231
aws_access_key_id="dummy",
aws_secret_access_key="dummy",
region_name="us-east-1",
)
def test_basic_no_exception(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="BasicOk",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
(transaction_event,) = envelopes
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "BasicOk"
assert transaction_event["sdk"]["name"] == "sentry.python.aws_lambda"
assert transaction_event["tags"] == {"aws_region": "us-east-1"}
assert transaction_event["extra"]["cloudwatch logs"] == {
"log_group": mock.ANY,
"log_stream": mock.ANY,
"url": mock.ANY,
}
assert transaction_event["extra"]["lambda"] == {
"aws_request_id": mock.ANY,
"execution_duration_in_millis": mock.ANY,
"function_name": "BasicOk",
"function_version": "$LATEST",
"invoked_function_arn": "arn:aws:lambda:us-east-1:012345678912:function:BasicOk",
"remaining_time_in_millis": mock.ANY,
}
assert transaction_event["contexts"]["trace"] == {
"op": "function.aws",
"description": mock.ANY,
"span_id": mock.ANY,
"parent_span_id": mock.ANY,
"trace_id": mock.ANY,
"origin": "auto.function.aws_lambda",
"data": mock.ANY,
}
def test_basic_exception(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="BasicException",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
# The second envelope we ignore.
# It is the transaction that we test in test_basic_no_exception.
(error_event, _) = envelopes
assert error_event["level"] == "error"
assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
assert error_event["exception"]["values"][0]["value"] == "Oh!"
assert error_event["sdk"]["name"] == "sentry.python.aws_lambda"
assert error_event["tags"] == {"aws_region": "us-east-1"}
assert error_event["extra"]["cloudwatch logs"] == {
"log_group": mock.ANY,
"log_stream": mock.ANY,
"url": mock.ANY,
}
assert error_event["extra"]["lambda"] == {
"aws_request_id": mock.ANY,
"execution_duration_in_millis": mock.ANY,
"function_name": "BasicException",
"function_version": "$LATEST",
"invoked_function_arn": "arn:aws:lambda:us-east-1:012345678912:function:BasicException",
"remaining_time_in_millis": mock.ANY,
}
assert error_event["contexts"]["trace"] == {
"op": "function.aws",
"description": mock.ANY,
"span_id": mock.ANY,
"parent_span_id": mock.ANY,
"trace_id": mock.ANY,
"origin": "auto.function.aws_lambda",
"data": mock.ANY,
}
def test_init_error(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="InitError",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
(error_event, transaction_event) = envelopes
assert (
error_event["exception"]["values"][0]["value"] == "name 'func' is not defined"
)
assert transaction_event["transaction"] == "InitError"
def test_timeout_error(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="TimeoutError",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
(error_event,) = envelopes
assert error_event["level"] == "error"
assert error_event["extra"]["lambda"]["function_name"] == "TimeoutError"
(exception,) = error_event["exception"]["values"]
assert not exception["mechanism"]["handled"]
assert exception["type"] == "ServerlessTimeoutWarning"
assert exception["value"].startswith(
"WARNING : Function is expected to get timed out. Configured timeout duration ="
)
assert exception["mechanism"]["type"] == "threading"
def test_timeout_error_scope_modified(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="TimeoutErrorScopeModified",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
(error_event,) = envelopes
assert error_event["level"] == "error"
assert (
error_event["extra"]["lambda"]["function_name"] == "TimeoutErrorScopeModified"
)
(exception,) = error_event["exception"]["values"]
assert not exception["mechanism"]["handled"]
assert exception["type"] == "ServerlessTimeoutWarning"
assert exception["value"].startswith(
"WARNING : Function is expected to get timed out. Configured timeout duration ="
)
assert exception["mechanism"]["type"] == "threading"
assert error_event["tags"]["custom_tag"] == "custom_value"
@pytest.mark.parametrize(
"aws_event, has_request_data, batch_size",
[
(b"1231", False, 1),
(b"11.21", False, 1),
(b'"Good dog!"', False, 1),
(b"true", False, 1),
(
b"""
[
{"good dog": "Maisey"},
{"good dog": "Charlie"},
{"good dog": "Cory"},
{"good dog": "Bodhi"}
]
""",
False,
4,
),
(
b"""
[
{
"headers": {
"Host": "x1.io",
"X-Forwarded-Proto": "https"
},
"httpMethod": "GET",
"path": "/1",
"queryStringParameters": {
"done": "f"
},
"d": "D1"
},
{
"headers": {
"Host": "x2.io",
"X-Forwarded-Proto": "http"
},
"httpMethod": "POST",
"path": "/2",
"queryStringParameters": {
"done": "t"
},
"d": "D2"
}
]
""",
True,
2,
),
(b"[]", False, 1),
],
ids=[
"event as integer",
"event as float",
"event as string",
"event as bool",
"event as list of dicts",
"event as dict",
"event as empty list",
],
)
def test_non_dict_event(
lambda_client, test_environment, aws_event, has_request_data, batch_size
):
lambda_client.invoke(
FunctionName="BasicException",
Payload=aws_event,
)
envelopes = test_environment["server"].envelopes
(error_event, transaction_event) = envelopes
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "BasicException"
assert transaction_event["sdk"]["name"] == "sentry.python.aws_lambda"
assert transaction_event["contexts"]["trace"]["status"] == "internal_error"
assert error_event["level"] == "error"
assert error_event["transaction"] == "BasicException"
assert error_event["sdk"]["name"] == "sentry.python.aws_lambda"
assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
assert error_event["exception"]["values"][0]["value"] == "Oh!"
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "aws_lambda"
if has_request_data:
request_data = {
"headers": {"Host": "x1.io", "X-Forwarded-Proto": "https"},
"method": "GET",
"url": "https://x1.io/1",
"query_string": {
"done": "f",
},
}
else:
request_data = {"url": "awslambda:///BasicException"}
assert error_event["request"] == request_data
assert transaction_event["request"] == request_data
if batch_size > 1:
assert error_event["tags"]["batch_size"] == batch_size
assert error_event["tags"]["batch_request"] is True
assert transaction_event["tags"]["batch_size"] == batch_size
assert transaction_event["tags"]["batch_request"] is True
def test_request_data(lambda_client, test_environment):
payload = b"""
{
"resource": "/asd",
"path": "/asd",
"httpMethod": "GET",
"headers": {
"Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
"User-Agent": "custom",
"X-Forwarded-Proto": "https"
},
"queryStringParameters": {
"bonkers": "true"
},
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"identity": {
"sourceIp": "213.47.147.207",
"userArn": "42"
}
},
"body": null,
"isBase64Encoded": false
}
"""
lambda_client.invoke(
FunctionName="BasicOk",
Payload=payload,
)
envelopes = test_environment["server"].envelopes
(transaction_event,) = envelopes
assert transaction_event["request"] == {
"headers": {
"Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com",
"User-Agent": "custom",
"X-Forwarded-Proto": "https",
},
"method": "GET",
"query_string": {"bonkers": "true"},
"url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd",
}
def test_trace_continuation(lambda_client, test_environment):
trace_id = "471a43a4192642f0b136d5159a501701"
parent_span_id = "6e8f22c393e68f19"
parent_sampled = 1
sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
# We simulate here AWS Api Gateway's behavior of passing HTTP headers
# as the `headers` dict in the event passed to the Lambda function.
payload = {
"headers": {
"sentry-trace": sentry_trace_header,
}
}
lambda_client.invoke(
FunctionName="BasicException",
Payload=json.dumps(payload),
)
envelopes = test_environment["server"].envelopes
(error_event, transaction_event) = envelopes
assert (
error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
@pytest.mark.parametrize(
"payload",
[
{},
{"headers": None},
{"headers": ""},
{"headers": {}},
{"headers": []}, # EventBridge sends an empty list
],
ids=[
"no headers",
"none headers",
"empty string headers",
"empty dict headers",
"empty list headers",
],
)
def test_headers(lambda_client, test_environment, payload):
lambda_client.invoke(
FunctionName="BasicException",
Payload=json.dumps(payload),
)
envelopes = test_environment["server"].envelopes
(error_event, _) = envelopes
assert error_event["level"] == "error"
assert error_event["exception"]["values"][0]["type"] == "RuntimeError"
assert error_event["exception"]["values"][0]["value"] == "Oh!"
def test_span_origin(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="BasicOk",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
(transaction_event,) = envelopes
assert (
transaction_event["contexts"]["trace"]["origin"] == "auto.function.aws_lambda"
)
def test_traces_sampler_has_correct_sampling_context(lambda_client, test_environment):
"""
Test that aws_event and aws_context are passed in the custom_sampling_context
when using the AWS Lambda integration.
"""
test_payload = {"test_key": "test_value"}
response = lambda_client.invoke(
FunctionName="TracesSampler",
Payload=json.dumps(test_payload),
)
response_payload = json.loads(response["Payload"].read().decode())
sampling_context_data = json.loads(response_payload["body"])[
"sampling_context_data"
]
assert sampling_context_data.get("aws_event_present") is True
assert sampling_context_data.get("aws_context_present") is True
assert sampling_context_data.get("event_data", {}).get("test_key") == "test_value"
@pytest.mark.parametrize(
"lambda_function_name",
["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"],
)
def test_error_has_new_trace_context(
lambda_client, test_environment, lambda_function_name
):
lambda_client.invoke(
FunctionName=lambda_function_name,
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
if lambda_function_name == "RaiseErrorPerformanceEnabled":
(error_event, transaction_event) = envelopes
else:
(error_event,) = envelopes
transaction_event = None
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
if transaction_event:
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
def _get_span_attr(attrs, key):
"""Extract the value from a span attribute, handling both flat and typed formats."""
val = attrs[key]
if isinstance(val, dict) and "value" in val:
return val["value"]
return val
def test_span_streaming_no_error(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="BasicOkSpanStreaming",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
span_items = test_environment["server"].span_items
assert len(envelopes) == 0
segment_spans = [s for s in span_items if s["is_segment"]]
assert len(segment_spans) == 1
segment_span = segment_spans[0]
assert segment_span["name"] == "BasicOkSpanStreaming"
attrs = segment_span["attributes"]
assert _get_span_attr(attrs, "sentry.op") == "function.aws"
assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda"
assert _get_span_attr(attrs, "sentry.span.source") == "component"
assert _get_span_attr(attrs, "cloud.provider") == "aws"
assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda"
assert (
_get_span_attr(attrs, "cloud.resource_id")
== "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming"
)
assert _get_span_attr(attrs, "cloud.region") == "us-east-1"
assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreaming"
assert _get_span_attr(attrs, "faas.version") == "$LATEST"
assert "faas.invocation_id" in attrs
assert (
_get_span_attr(attrs, "aws.lambda.invoked_arn")
== "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming"
)
assert _get_span_attr(attrs, "aws.log.group.names") == [
"aws/lambda/BasicOkSpanStreaming"
]
assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"]
assert _get_span_attr(attrs, "messaging.batch.message_count") == 1
def test_span_streaming_error(lambda_client, test_environment):
lambda_client.invoke(
FunctionName="RaiseErrorSpanStreaming",
Payload=json.dumps({}),
)
envelopes = test_environment["server"].envelopes
span_items = test_environment["server"].span_items
assert len(envelopes) == 1
error_event = envelopes[0]
assert error_event["level"] == "error"
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh!"
assert exception["mechanism"]["type"] == "aws_lambda"
assert not exception["mechanism"]["handled"]
segment_spans = [s for s in span_items if s["is_segment"]]
assert len(segment_spans) == 1
segment_span = segment_spans[0]
assert segment_span["name"] == "RaiseErrorSpanStreaming"
assert segment_span["status"] == "error"
attrs = segment_span["attributes"]
assert _get_span_attr(attrs, "sentry.op") == "function.aws"
assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda"
assert _get_span_attr(attrs, "sentry.span.source") == "component"
assert _get_span_attr(attrs, "cloud.provider") == "aws"
assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda"
assert (
_get_span_attr(attrs, "cloud.resource_id")
== "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming"
)
assert _get_span_attr(attrs, "cloud.region") == "us-east-1"
assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming"
assert _get_span_attr(attrs, "faas.version") == "$LATEST"
assert "faas.invocation_id" in attrs
assert (
_get_span_attr(attrs, "aws.lambda.invoked_arn")
== "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming"
)
assert _get_span_attr(attrs, "aws.log.group.names") == [
"aws/lambda/RaiseErrorSpanStreaming"
]
assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"]
assert _get_span_attr(attrs, "messaging.batch.message_count") == 1
def test_span_streaming_trace_continuation(lambda_client, test_environment):
trace_id = "471a43a4192642f0b136d5159a501701"
parent_span_id = "6e8f22c393e68f19"
parent_sampled = 1
sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
payload = {
"headers": {
"sentry-trace": sentry_trace_header,
}
}
lambda_client.invoke(
FunctionName="RaiseErrorSpanStreaming",
Payload=json.dumps(payload),
)
envelopes = test_environment["server"].envelopes
span_items = test_environment["server"].span_items
assert len(envelopes) == 1
error_event = envelopes[0]
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
segment_spans = [s for s in span_items if s["is_segment"]]
assert len(segment_spans) == 1
segment_span = segment_spans[0]
assert segment_span["trace_id"] == trace_id
assert segment_span["name"] == "RaiseErrorSpanStreaming"
attrs = segment_span["attributes"]
assert _get_span_attr(attrs, "sentry.op") == "function.aws"
assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda"
assert _get_span_attr(attrs, "sentry.span.source") == "component"
assert _get_span_attr(attrs, "cloud.provider") == "aws"
assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda"
assert _get_span_attr(attrs, "cloud.region") == "us-east-1"
assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming"
assert _get_span_attr(attrs, "faas.version") == "$LATEST"
assert "faas.invocation_id" in attrs
def test_span_streaming_request_attributes(lambda_client, test_environment):
payload = {
"headers": {
"Content-Type": "application/json",
"Accept": "text/html",
},
"httpMethod": "POST",
"queryStringParameters": {"foo": "bar", "a-complicated-value": "a=b&c=d"},
"path": "/test",
}
lambda_client.invoke(
FunctionName="BasicOkSpanStreamingPii",
Payload=json.dumps(payload),
)
span_items = test_environment["server"].span_items
segment_spans = [s for s in span_items if s["is_segment"]]
assert len(segment_spans) == 1
segment_span = segment_spans[0]
attrs = segment_span["attributes"]
assert _get_span_attr(attrs, "http.request.method") == "POST"
assert (
_get_span_attr(attrs, "url.query")
== "foo=bar&a-complicated-value=a%3Db%26c%3Dd"
)
assert (
_get_span_attr(attrs, "http.request.header.content-type") == "application/json"
)
assert _get_span_attr(attrs, "http.request.header.accept") == "text/html"
assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreamingPii"
assert _get_span_attr(attrs, "cloud.provider") == "aws"
assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda"
assert _get_span_attr(attrs, "cloud.region") == "us-east-1"
assert _get_span_attr(attrs, "faas.version") == "$LATEST"
assert "faas.invocation_id" in attrs
assert _get_span_attr(attrs, "aws.log.group.names") == [
"aws/lambda/BasicOkSpanStreamingPii"
]
assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"]
@pytest.mark.parametrize(
"lambda_function_name",
["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"],
)
def test_error_has_existing_trace_context(
lambda_client, test_environment, lambda_function_name
):
trace_id = "471a43a4192642f0b136d5159a501701"
parent_span_id = "6e8f22c393e68f19"
parent_sampled = 1
sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
# We simulate here AWS Api Gateway's behavior of passing HTTP headers
# as the `headers` dict in the event passed to the Lambda function.
payload = {
"headers": {
"sentry-trace": sentry_trace_header,
}
}
lambda_client.invoke(
FunctionName=lambda_function_name,
Payload=json.dumps(payload),
)
envelopes = test_environment["server"].envelopes
if lambda_function_name == "RaiseErrorPerformanceEnabled":
(error_event, transaction_event) = envelopes
else:
(error_event,) = envelopes
transaction_event = None
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
error_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
if transaction_event:
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
transaction_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
sentry-python-2.64.0/tests/integrations/aws_lambda/utils.py 0000664 0000000 0000000 00000022764 15220673775 0024153 0 ustar 00root root 0000000 0000000 import gzip
import json
import os
import platform
import shutil
import socket
import subprocess
import sys
import threading
import time
import requests
import uvicorn
from aws_cdk import (
CfnResource,
Stack,
)
from constructs import Construct
from fastapi import FastAPI, Request
from scripts.build_aws_lambda_layer import DIST_PATH, build_packaged_zip
LAMBDA_FUNCTION_DIR = "./tests/integrations/aws_lambda/lambda_functions/"
LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR = (
"./tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/"
)
LAMBDA_FUNCTION_TIMEOUT = 10
SAM_PORT = 3001
PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}"
def get_host_ip():
"""
Returns the IP address of the host we are running on.
"""
if os.environ.get("GITHUB_ACTIONS"):
# Running in GitHub Actions
hostname = socket.gethostname()
host = socket.gethostbyname(hostname)
else:
# Running locally
if platform.system() in ["Darwin", "Windows"]:
# Windows or MacOS
host = "host.docker.internal"
else:
# Linux
hostname = socket.gethostname()
host = socket.gethostbyname(hostname)
return host
class LocalLambdaStack(Stack):
"""
Uses the AWS CDK to create a local SAM stack containing Lambda functions.
"""
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
print("[LocalLambdaStack] Creating local SAM Lambda Stack")
super().__init__(scope, construct_id, **kwargs)
# Override the template synthesis
self.template_options.template_format_version = "2010-09-09"
self.template_options.transforms = ["AWS::Serverless-2016-10-31"]
print("[LocalLambdaStack] Create Sentry Lambda layer package")
filename = "sentry-sdk-lambda-layer.zip"
subprocess.check_call(["uv", "build", "-o", DIST_PATH])
build_packaged_zip(out_zip_filename=filename)
print(
"[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack"
)
self.sentry_layer = CfnResource(
self,
"SentryPythonServerlessSDK",
type="AWS::Serverless::LayerVersion",
properties={
"ContentUri": os.path.join(DIST_PATH, filename),
"CompatibleRuntimes": [
PYTHON_VERSION,
],
},
)
dsn = f"http://123@{get_host_ip()}:9999/0" # noqa: E231
print("[LocalLambdaStack] Using Sentry DSN: %s" % dsn)
print(
"[LocalLambdaStack] Add all Lambda functions defined in "
"/tests/integrations/aws_lambda/lambda_functions/ to the SAM stack"
)
lambda_dirs = [
d
for d in os.listdir(LAMBDA_FUNCTION_DIR)
if os.path.isdir(os.path.join(LAMBDA_FUNCTION_DIR, d))
]
for lambda_dir in lambda_dirs:
CfnResource(
self,
lambda_dir,
type="AWS::Serverless::Function",
properties={
"CodeUri": os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
"Handler": "sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler",
"Runtime": PYTHON_VERSION,
"Timeout": LAMBDA_FUNCTION_TIMEOUT,
"Layers": [
{"Ref": self.sentry_layer.logical_id}
], # Add layer containing the Sentry SDK to function.
"Environment": {
"Variables": {
"SENTRY_DSN": dsn,
"SENTRY_INITIAL_HANDLER": "index.handler",
"SENTRY_TRACES_SAMPLE_RATE": "1.0",
}
},
},
)
print(
"[LocalLambdaStack] - Created Lambda function: %s (%s)"
% (
lambda_dir,
os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
)
)
print(
"[LocalLambdaStack] Add all Lambda functions defined in "
"/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/ to the SAM stack"
)
lambda_dirs = [
d
for d in os.listdir(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR)
if os.path.isdir(os.path.join(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, d))
]
for lambda_dir in lambda_dirs:
# Copy the Sentry SDK into the function directory
sdk_path = os.path.join(
LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir, "sentry_sdk"
)
if not os.path.exists(sdk_path):
# Find the Sentry SDK in the current environment
import sentry_sdk as sdk_module
sdk_source = os.path.dirname(sdk_module.__file__)
shutil.copytree(sdk_source, sdk_path)
# Install the requirements of Sentry SDK into the function directory
subprocess.check_call(
[
"uv",
"pip",
"install",
"--upgrade",
"--group",
"aws",
"--target",
os.path.join(LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir),
]
)
CfnResource(
self,
lambda_dir,
type="AWS::Serverless::Function",
properties={
"CodeUri": os.path.join(
LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir
),
"Handler": "index.handler",
"Runtime": PYTHON_VERSION,
"Timeout": LAMBDA_FUNCTION_TIMEOUT,
"Environment": {
"Variables": {
"SENTRY_DSN": dsn,
}
},
},
)
print(
"[LocalLambdaStack] - Created Lambda function: %s (%s)"
% (
lambda_dir,
os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir),
)
)
@classmethod
def wait_for_stack(cls, timeout=60, port=SAM_PORT):
"""
Wait for SAM to be ready, with timeout.
"""
start_time = time.time()
while True:
if time.time() - start_time > timeout:
raise TimeoutError(
"AWS SAM failed to start within %s seconds. (Maybe Docker is not running?)"
% timeout
)
try:
# Try to connect to SAM
response = requests.get(f"http://127.0.0.1:{port}/") # noqa: E231
if response.status_code == 200 or response.status_code == 404:
return
except requests.exceptions.ConnectionError:
time.sleep(1)
continue
class SentryServerForTesting:
"""
A simple Sentry.io style server that accepts envelopes and stores them in a list.
"""
def __init__(self, host="0.0.0.0", port=9999, log_level="warning"):
self.envelopes = []
self.span_items = []
self.host = host
self.port = port
self.log_level = log_level
self.app = FastAPI()
@self.app.post("/api/0/envelope/")
async def envelope(request: Request):
print("[SentryServerForTesting] Received envelope")
try:
raw_body = await request.body()
except Exception:
return {"status": "no body received"}
try:
body = gzip.decompress(raw_body).decode("utf-8")
except Exception:
# If decompression fails, assume it's plain text
body = raw_body.decode("utf-8")
lines = body.split("\n")
current_line = 1 # line 0 is envelope header
while current_line < len(lines):
# skip empty lines
if not lines[current_line].strip():
current_line += 1
continue
# parse envelope item header
item_header = json.loads(lines[current_line])
current_line += 1
# parse envelope item payload
if current_line < len(lines) and lines[current_line].strip():
parsed_item = json.loads(lines[current_line])
if item_header.get("type") == "span":
if "items" in parsed_item:
self.span_items.extend(parsed_item["items"])
else:
self.span_items.append(parsed_item)
else:
self.envelopes.append(parsed_item)
current_line += 1
return {"status": "ok"}
def run_server(self):
uvicorn.run(self.app, host=self.host, port=self.port, log_level=self.log_level)
def start(self):
print(
"[SentryServerForTesting] Starting server on %s:%s" % (self.host, self.port)
)
server_thread = threading.Thread(target=self.run_server, daemon=True)
server_thread.start()
def clear_envelopes(self):
print("[SentryServerForTesting] Clearing envelopes")
self.envelopes = []
self.span_items = []
sentry-python-2.64.0/tests/integrations/beam/ 0000775 0000000 0000000 00000000000 15220673775 0021240 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/beam/__init__.py 0000664 0000000 0000000 00000000062 15220673775 0023347 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("apache_beam")
sentry-python-2.64.0/tests/integrations/beam/test_beam.py 0000664 0000000 0000000 00000013565 15220673775 0023567 0 ustar 00root root 0000000 0000000 import inspect
import dill
import pytest
from apache_beam.runners.common import DoFnContext, DoFnInvoker
from apache_beam.transforms.core import CallableWrapperDoFn, DoFn, ParDo, _DoFnParam
from apache_beam.typehints.decorators import getcallargs_forhints
from apache_beam.typehints.trivial_inference import instance_to_type
from apache_beam.utils.windowed_value import WindowedValue
from sentry_sdk.integrations.beam import (
BeamIntegration,
_wrap_inspect_call,
_wrap_task_call,
)
try:
from apache_beam.runners.common import OutputHandler
except ImportError:
from apache_beam.runners.common import OutputProcessor as OutputHandler
def foo():
return True
def bar(x, y):
# print(x + y)
return True
def baz(x, y=2):
# print(x + y)
return True
class A:
def __init__(self, fn):
self.r = "We are in A"
self.fn = fn
self._inspect_fn = _wrap_inspect_call(self, "fn")
def process(self):
return self.fn()
class B(A):
def fa(self, x, element=False, another_element=False):
if x or (element and not another_element):
# print(self.r)
return True
1 / 0
return False
def __init__(self):
self.r = "We are in B"
super().__init__(self.fa)
class SimpleFunc(DoFn):
def process(self, x):
if x:
1 / 0
return [True]
class PlaceHolderFunc(DoFn):
def process(self, x, timestamp=DoFn.TimestampParam, wx=DoFn.WindowParam):
if isinstance(timestamp, _DoFnParam) or isinstance(wx, _DoFnParam):
raise Exception("Bad instance")
if x:
1 / 0
yield True
def fail(x):
if x:
1 / 0
return [True]
test_parent = A(foo)
test_child = B()
test_simple = SimpleFunc()
test_place_holder = PlaceHolderFunc()
test_callable = CallableWrapperDoFn(fail)
# Cannot call simple functions or placeholder test.
@pytest.mark.parametrize(
"obj,f,args,kwargs",
[
[test_parent, "fn", (), {}],
[test_child, "fn", (False,), {"element": True}],
[test_child, "fn", (True,), {}],
[test_simple, "process", (False,), {}],
[test_callable, "process", (False,), {}],
],
)
def test_monkey_patch_call(obj, f, args, kwargs):
func = getattr(obj, f)
assert func(*args, **kwargs)
assert _wrap_task_call(func)(*args, **kwargs)
@pytest.mark.parametrize("f", [foo, bar, baz, test_parent.fn, test_child.fn])
def test_monkey_patch_pickle(f):
f_temp = _wrap_task_call(f)
assert dill.pickles(f_temp), "{} is not pickling correctly!".format(f)
# Pickle everything
s1 = dill.dumps(f_temp)
s2 = dill.loads(s1)
dill.dumps(s2)
@pytest.mark.parametrize(
"f,args,kwargs",
[
[foo, (), {}],
[bar, (1, 5), {}],
[baz, (1,), {}],
[test_parent.fn, (), {}],
[test_child.fn, (False,), {"element": True}],
[test_child.fn, (True,), {}],
],
)
def test_monkey_patch_signature(f, args, kwargs):
arg_types = [instance_to_type(v) for v in args]
kwargs_types = {k: instance_to_type(v) for (k, v) in kwargs.items()}
f_temp = _wrap_task_call(f)
try:
getcallargs_forhints(f, *arg_types, **kwargs_types)
except Exception:
print("Failed on {} with parameters {}, {}".format(f, args, kwargs))
raise
try:
getcallargs_forhints(f_temp, *arg_types, **kwargs_types)
except Exception:
print("Failed on {} with parameters {}, {}".format(f_temp, args, kwargs))
raise
try:
expected_signature = inspect.signature(f)
test_signature = inspect.signature(f_temp)
assert expected_signature == test_signature, (
"Failed on {}, signature {} does not match {}".format(
f, expected_signature, test_signature
)
)
except Exception:
# expected to pass for py2.7
pass
class _OutputHandler(OutputHandler):
def process_outputs(
self, windowed_input_element, results, watermark_estimator=None
):
self.handle_process_outputs(
windowed_input_element, results, watermark_estimator
)
def handle_process_outputs(
self, windowed_input_element, results, watermark_estimator=None
):
print(windowed_input_element)
try:
for result in results:
assert result
except StopIteration:
print("In here")
@pytest.fixture
def init_beam(sentry_init):
def inner(fn):
sentry_init(default_integrations=False, integrations=[BeamIntegration()])
# Little hack to avoid having to run the whole pipeline.
pardo = ParDo(fn)
signature = pardo._signature
output_processor = _OutputHandler()
return DoFnInvoker.create_invoker(
signature,
output_processor,
DoFnContext("test"),
input_args=[],
input_kwargs={},
)
return inner
@pytest.mark.parametrize("fn", [test_simple, test_callable, test_place_holder])
def test_invoker_normal(init_beam, fn):
invoker = init_beam(fn)
print("Normal testing {} with {} invoker.".format(fn, invoker))
windowed_value = WindowedValue(False, 0, [None])
invoker.invoke_process(windowed_value)
@pytest.mark.parametrize("fn", [test_simple, test_callable, test_place_holder])
def test_invoker_exception(init_beam, capture_events, capture_exceptions, fn):
invoker = init_beam(fn)
events = capture_events()
print("Exception testing {} with {} invoker.".format(fn, invoker))
# Window value will always have one value for the process to run.
windowed_value = WindowedValue(True, 0, [None])
try:
invoker.invoke_process(windowed_value)
except Exception:
pass
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "beam"
sentry-python-2.64.0/tests/integrations/boto3/ 0000775 0000000 0000000 00000000000 15220673775 0021362 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/boto3/__init__.py 0000664 0000000 0000000 00000000347 15220673775 0023477 0 ustar 00root root 0000000 0000000 import os
import pytest
pytest.importorskip("boto3")
xml_fixture_path = os.path.dirname(os.path.abspath(__file__))
def read_fixture(name):
with open(os.path.join(xml_fixture_path, name), "rb") as f:
return f.read()
sentry-python-2.64.0/tests/integrations/boto3/aws_mock.py 0000664 0000000 0000000 00000001554 15220673775 0023544 0 ustar 00root root 0000000 0000000 from io import BytesIO
from botocore.awsrequest import AWSResponse
class Body(BytesIO):
def stream(self, **kwargs):
contents = self.read()
while contents:
yield contents
contents = self.read()
class MockResponse:
def __init__(self, client, status_code, headers, body):
self._client = client
self._status_code = status_code
self._headers = headers
self._body = body
def __enter__(self):
self._client.meta.events.register("before-send", self)
return self
def __exit__(self, exc_type, exc_value, traceback):
self._client.meta.events.unregister("before-send", self)
def __call__(self, request, **kwargs):
return AWSResponse(
request.url,
self._status_code,
self._headers,
Body(self._body),
)
sentry-python-2.64.0/tests/integrations/boto3/s3_list.xml 0000664 0000000 0000000 00000001545 15220673775 0023471 0 ustar 00root root 0000000 0000000
marshalls-furious-bucket1000urlfalsefoo.txt2020-10-24T00:13:39.000Z"a895ba674b4abd01b5d67cfd7074b827"2064537bef397f7e536914d1ff1bbdb105ed90bcfd06269456bf4a06c6e2e54564daf7STANDARDbar.txt2020-10-02T15:15:20.000Z"a895ba674b4abd01b5d67cfd7074b827"2064537bef397f7e536914d1ff1bbdb105ed90bcfd06269456bf4a06c6e2e54564daf7STANDARD
sentry-python-2.64.0/tests/integrations/boto3/test_s3.py 0000664 0000000 0000000 00000027666 15220673775 0023341 0 ustar 00root root 0000000 0000000 from unittest import mock
import boto3
import pytest
import sentry_sdk
from sentry_sdk.integrations.boto3 import Boto3Integration
from tests.conftest import ApproxDict
from tests.integrations.boto3 import read_fixture
from tests.integrations.boto3.aws_mock import MockResponse
session = boto3.Session(
aws_access_key_id="-",
aws_secret_access_key="-",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_basic(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
# disabled because session.resource() or s3.Bucket() result in a subprocess span for a
# shell that runs "uname -p 2> /dev/null" on Python 3.7 with boto3 version 1.12.49.
default_integrations=False,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
s3 = session.resource("s3")
bucket = s3.Bucket("bucket")
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
objects = [obj for obj in bucket.objects.all()]
assert len(objects) == 2
assert objects[0].key == "foo.txt"
assert objects[1].key == "bar.txt"
span.end()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 2
span = spans[0]
assert span["attributes"]["sentry.op"] == "http.client"
assert span["name"] == "aws.s3.ListObjects"
else:
events = capture_events()
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
items = [obj for obj in bucket.objects.all()]
assert len(items) == 2
assert items[0].key == "foo.txt"
assert items[1].key == "bar.txt"
transaction.finish()
(event,) = events
assert event["type"] == "transaction"
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == "http.client"
assert span["description"] == "aws.s3.ListObjects"
@pytest.mark.parametrize("send_default_pii", [True, False])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_streaming(
sentry_init,
capture_events,
capture_items,
span_streaming,
send_default_pii,
):
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
s3 = session.resource("s3")
obj = s3.Bucket("bucket").Object("foo.pdf")
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
body = obj.get()["Body"]
assert body.read(1) == b"h"
assert body.read(2) == b"el"
assert body.read(3) == b"lo"
assert body.read(1) == b""
span.end()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
span1 = spans[0]
assert span1["attributes"]["sentry.op"] == "http.client"
assert span1["name"] == "aws.s3.GetObject"
expected_attrs = {
"http.request.method": "GET",
"rpc.method": "S3/GetObject",
"sentry.environment": "production",
"sentry.op": "http.client",
"sentry.origin": "auto.http.boto3",
"sentry.release": mock.ANY,
"sentry.sdk.name": "sentry.python",
"sentry.sdk.version": mock.ANY,
"sentry.segment.id": mock.ANY,
"sentry.segment.name": "custom parent",
"server.address": mock.ANY,
"thread.id": mock.ANY,
"thread.name": mock.ANY,
}
if send_default_pii:
expected_attrs["url.full"] = "https://bucket.s3.amazonaws.com/foo.pdf"
expected_attrs["url.fragment"] = ""
expected_attrs["url.query"] = ""
assert span1["attributes"] == ApproxDict(expected_attrs)
if not send_default_pii:
assert "url.full" not in span1["attributes"]
assert "url.fragment" not in span1["attributes"]
assert "url.query" not in span1["attributes"]
span2 = spans[1]
assert span2["attributes"]["sentry.op"] == "http.client.stream"
assert span2["name"] == "aws.s3.GetObject"
assert span2["parent_span_id"] == span1["span_id"]
else:
events = capture_events()
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
body = obj.get()["Body"]
assert body.read(1) == b"h"
assert body.read(2) == b"el"
assert body.read(3) == b"lo"
assert body.read(1) == b""
transaction.finish()
(event,) = events
assert event["type"] == "transaction"
assert len(event["spans"]) == 2
span1 = event["spans"][0]
assert span1["op"] == "http.client"
assert span1["description"] == "aws.s3.GetObject"
assert span1["data"] == ApproxDict(
{
"http.method": "GET",
"aws.request.url": "https://bucket.s3.amazonaws.com/foo.pdf",
"http.fragment": "",
"http.query": "",
}
)
span2 = event["spans"][1]
assert span2["op"] == "http.client.stream"
assert span2["description"] == "aws.s3.GetObject"
assert span2["parent_span_id"] == span1["span_id"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_streaming_close(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
s3 = session.resource("s3")
obj = s3.Bucket("bucket").Object("foo.pdf")
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
body = obj.get()["Body"]
assert body.read(1) == b"h"
body.close() # close partially-read stream
span.end()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 3
span1 = spans[0]
assert span1["attributes"]["sentry.op"] == "http.client"
span2 = spans[1]
assert span2["attributes"]["sentry.op"] == "http.client.stream"
else:
events = capture_events()
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
body = obj.get()["Body"]
assert body.read(1) == b"h"
body.close() # close partially-read stream
transaction.finish()
(event,) = events
assert event["type"] == "transaction"
assert len(event["spans"]) == 2
span1 = event["spans"][0]
assert span1["op"] == "http.client"
span2 = event["spans"][1]
assert span2["op"] == "http.client.stream"
@pytest.mark.tests_internal_exceptions
@pytest.mark.parametrize("span_streaming", [True, False])
def test_omit_url_data_if_parsing_fails(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
s3 = session.resource("s3")
bucket = s3.Bucket("bucket")
if span_streaming:
items = capture_items("span")
with mock.patch(
"sentry_sdk.integrations.boto3.parse_url",
side_effect=ValueError,
):
with sentry_sdk.traces.start_span(
name="custom parent"
) as span, MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
objects = [obj for obj in bucket.objects.all()]
assert len(objects) == 2
assert objects[0].key == "foo.txt"
assert objects[1].key == "bar.txt"
span.end()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[0]["attributes"] == ApproxDict(
{
"http.request.method": "GET",
"rpc.method": "S3/ListObjects",
"sentry.environment": "production",
"sentry.op": "http.client",
"sentry.origin": "auto.http.boto3",
"sentry.release": mock.ANY,
"sentry.sdk.name": "sentry.python",
"sentry.sdk.version": mock.ANY,
"sentry.segment.id": mock.ANY,
"sentry.segment.name": "custom parent",
"server.address": mock.ANY,
"thread.id": mock.ANY,
"thread.name": mock.ANY,
}
)
assert "url.full" not in spans[0]["attributes"]
assert "url.fragment" not in spans[0]["attributes"]
assert "url.query" not in spans[0]["attributes"]
else:
events = capture_events()
with mock.patch(
"sentry_sdk.integrations.boto3.parse_url",
side_effect=ValueError,
):
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
items = [obj for obj in bucket.objects.all()]
assert len(items) == 2
assert items[0].key == "foo.txt"
assert items[1].key == "bar.txt"
transaction.finish()
(event,) = events
assert event["spans"][0]["data"] == ApproxDict(
{
"http.method": "GET",
# no url data
}
)
assert "aws.request.url" not in event["spans"][0]["data"]
assert "http.fragment" not in event["spans"][0]["data"]
assert "http.query" not in event["spans"][0]["data"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
s3 = session.resource("s3")
bucket = s3.Bucket("bucket")
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"), MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
_ = [obj for obj in bucket.objects.all()]
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[1]["attributes"]["sentry.origin"] == "manual"
assert spans[0]["attributes"]["sentry.origin"] == "auto.http.boto3"
else:
events = capture_events()
with sentry_sdk.start_transaction(), MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
_ = [obj for obj in bucket.objects.all()]
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.http.boto3"
sentry-python-2.64.0/tests/integrations/bottle/ 0000775 0000000 0000000 00000000000 15220673775 0021625 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/bottle/__init__.py 0000664 0000000 0000000 00000000055 15220673775 0023736 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("bottle")
sentry-python-2.64.0/tests/integrations/bottle/test_bottle.py 0000664 0000000 0000000 00000051415 15220673775 0024535 0 ustar 00root root 0000000 0000000 import json
import logging
from io import BytesIO
import pytest
from bottle import Bottle, HTTPResponse, abort, redirect
from bottle import debug as set_debug
from werkzeug.test import Client
from werkzeug.wrappers import Response
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations.bottle import BottleIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
@pytest.fixture(scope="function")
def app(sentry_init):
app = Bottle()
@app.route("/message")
def hi():
capture_message("hi")
return "ok"
@app.route("/message/")
def hi_with_id(message_id):
capture_message("hi")
return "ok"
@app.route("/message-named-route", name="hi")
def named_hi():
capture_message("hi")
return "ok"
yield app
@pytest.fixture
def get_client(app):
def inner():
return Client(app)
return inner
def test_has_context(sentry_init, app, capture_events, get_client):
sentry_init(integrations=[BottleIntegration()])
events = capture_events()
client = get_client()
response = client.get("/message")
assert response[1] == "200 OK"
(event,) = events
assert event["message"] == "hi"
assert "data" not in event["request"]
assert event["request"]["url"] == "http://localhost/message"
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
("/message", "endpoint", "hi", "component"),
("/message", "url", "/message", "route"),
("/message/123456", "url", "/message/", "route"),
("/message-named-route", "endpoint", "hi", "component"),
],
)
def test_transaction_style(
sentry_init,
url,
transaction_style,
expected_transaction,
expected_source,
capture_events,
get_client,
):
sentry_init(integrations=[BottleIntegration(transaction_style=transaction_style)])
events = capture_events()
client = get_client()
response = client.get(url)
assert response[1] == "200 OK"
(event,) = events
# We use endswith() because in Python 2.7 it is "test_bottle.hi"
# and in later Pythons "test_bottle.app..hi"
assert event["transaction"].endswith(expected_transaction)
assert event["transaction_info"] == {"source": expected_source}
@pytest.mark.parametrize("debug", (True, False), ids=["debug", "nodebug"])
@pytest.mark.parametrize("catchall", (True, False), ids=["catchall", "nocatchall"])
def test_errors(
sentry_init, capture_exceptions, capture_events, app, debug, catchall, get_client
):
sentry_init(integrations=[BottleIntegration()])
app.catchall = catchall
set_debug(mode=debug)
exceptions = capture_exceptions()
events = capture_events()
@app.route("/")
def index():
1 / 0
client = get_client()
try:
client.get("/")
except ZeroDivisionError:
pass
(exc,) = exceptions
assert isinstance(exc, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "bottle"
assert event["exception"]["values"][0]["mechanism"]["handled"] is False
@pytest.mark.parametrize("max_value_length", [1024, None])
def test_large_json_request(
sentry_init, capture_events, app, get_client, max_value_length
):
sentry_init(
integrations=[BottleIntegration()],
max_request_body_size="always",
max_value_length=max_value_length,
)
data = {"foo": {"bar": "a" * (1034)}}
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.json == data
assert bottle.request.body.read() == json.dumps(data).encode("ascii")
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.get("/")
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response[1] == "200 OK"
(event,) = events
if max_value_length:
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {
"len": 1034,
"rem": [["!limit", "x", 1021, 1024]],
}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
else:
assert len(event["request"]["data"]["foo"]["bar"]) == 1034
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
def test_empty_json_request(sentry_init, capture_events, app, data, get_client):
sentry_init(integrations=[BottleIntegration()])
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.json == data
assert bottle.request.body.read() == json.dumps(data).encode("ascii")
# assert not bottle.request.forms
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response[1] == "200 OK"
(event,) = events
assert event["request"]["data"] == data
@pytest.mark.parametrize("max_value_length", [1024, None])
def test_medium_formdata_request(
sentry_init, capture_events, app, get_client, max_value_length
):
sentry_init(
integrations=[BottleIntegration()],
max_request_body_size="always",
max_value_length=max_value_length,
)
data = {"foo": "a" * (1034)}
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.forms["foo"] == data["foo"]
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
(event,) = events
if max_value_length:
assert event["_meta"]["request"]["data"]["foo"] == {
"": {
"len": 1034,
"rem": [["!limit", "x", 1021, 1024]],
}
}
assert len(event["request"]["data"]["foo"]) == 1024
else:
assert len(event["request"]["data"]["foo"]) == 1034
@pytest.mark.parametrize("input_char", ["a", b"a"])
def test_too_large_raw_request(
sentry_init, input_char, capture_events, app, get_client
):
sentry_init(integrations=[BottleIntegration()], max_request_body_size="small")
data = input_char * 2000
@app.route("/", method="POST")
def index():
import bottle
if isinstance(data, bytes):
assert bottle.request.body.read() == data
else:
assert bottle.request.body.read() == data.encode("ascii")
assert not bottle.request.json
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
(event,) = events
assert event["_meta"]["request"]["data"] == {"": {"rem": [["!config", "x"]]}}
assert not event["request"]["data"]
@pytest.mark.parametrize("max_value_length", [1024, None])
def test_files_and_form(sentry_init, capture_events, app, get_client, max_value_length):
sentry_init(
integrations=[BottleIntegration()],
max_request_body_size="always",
max_value_length=max_value_length,
)
data = {
"foo": "a" * (1034),
"file": (BytesIO(b"hello"), "hello.txt"),
}
@app.route("/", method="POST")
def index():
import bottle
assert list(bottle.request.forms) == ["foo"]
assert list(bottle.request.files) == ["file"]
assert not bottle.request.json
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", data=data)
assert response[1] == "200 OK"
(event,) = events
if max_value_length:
assert event["_meta"]["request"]["data"]["foo"] == {
"": {
"len": 1034,
"rem": [["!limit", "x", 1021, 1024]],
}
}
assert len(event["request"]["data"]["foo"]) == 1024
else:
assert len(event["request"]["data"]["foo"]) == 1034
assert event["_meta"]["request"]["data"]["file"] == {
"": {
"rem": [["!raw", "x"]],
}
}
assert not event["request"]["data"]["file"]
def test_json_not_truncated_if_max_request_body_size_is_always(
sentry_init, capture_events, app, get_client
):
sentry_init(integrations=[BottleIntegration()], max_request_body_size="always")
data = {
"key{}".format(i): "value{}".format(i) for i in range(MAX_DATABAG_BREADTH + 10)
}
@app.route("/", method="POST")
def index():
import bottle
assert bottle.request.json == data
assert bottle.request.body.read() == json.dumps(data).encode("ascii")
capture_message("hi")
return "ok"
events = capture_events()
client = get_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response[1] == "200 OK"
(event,) = events
assert event["request"]["data"] == data
@pytest.mark.parametrize(
"integrations",
[
[BottleIntegration()],
[BottleIntegration(), LoggingIntegration(event_level="ERROR")],
],
)
def test_errors_not_reported_twice(
sentry_init, integrations, capture_events, app, get_client
):
sentry_init(integrations=integrations)
app.catchall = False
logger = logging.getLogger("bottle.app")
@app.route("/")
def index():
1 / 0
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
try:
client.get("/")
except ZeroDivisionError as e:
logger.exception(e)
raise e
assert len(events) == 1
def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client):
sentry_init(integrations=[BottleIntegration()])
app.catchall = False
def crashing_app(environ, start_response):
1 / 0
app.mount("/wsgi/", crashing_app)
client = Client(app)
exceptions = capture_exceptions()
events = capture_events()
with pytest.raises(ZeroDivisionError) as exc:
client.get("/wsgi/")
(error,) = exceptions
assert error is exc.value
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "bottle"
assert event["exception"]["values"][0]["mechanism"]["handled"] is False
def test_error_in_errorhandler(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[BottleIntegration()])
set_debug(False)
app.catchall = True
@app.route("/")
def index():
raise ValueError()
@app.error(500)
def error_handler(err):
1 / 0
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
event1, event2 = events
(exception,) = event1["exception"]["values"]
assert exception["type"] == "ValueError"
exception = event2["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
def test_bad_request_not_captured(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[BottleIntegration()])
events = capture_events()
@app.route("/")
def index():
abort(400, "bad request in")
client = get_client()
client.get("/")
assert not events
def test_no_exception_on_redirect(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[BottleIntegration()])
events = capture_events()
@app.route("/")
def index():
redirect("/here")
@app.route("/here")
def here():
return "here"
client = get_client()
client.get("/")
assert not events
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(
sentry_init,
get_client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[BottleIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
else:
events = capture_events()
client = get_client()
client.get("/message")
if span_streaming:
sentry_sdk.flush()
spans = [item.payload for item in items]
segment = spans[-1]
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.origin"] == "auto.http.bottle"
else:
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.bottle"
@pytest.mark.parametrize("raise_error", [True, False])
@pytest.mark.parametrize(
("integration_kwargs", "status_code", "should_capture"),
(
({}, None, False),
({}, 400, False),
({}, 451, False), # Highest 4xx status code
({}, 500, True),
({}, 511, True), # Highest 5xx status code
({"failed_request_status_codes": set()}, 500, False),
({"failed_request_status_codes": set()}, 511, False),
({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True),
({"failed_request_status_codes": {404, *range(500, 600)}}, 500, True),
({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False),
),
)
def test_failed_request_status_codes(
sentry_init,
capture_events,
integration_kwargs,
status_code,
should_capture,
raise_error,
):
sentry_init(integrations=[BottleIntegration(**integration_kwargs)])
events = capture_events()
app = Bottle()
@app.route("/")
def handle():
if status_code is not None:
response = HTTPResponse(status=status_code)
if raise_error:
raise response
else:
return response
return "OK"
client = Client(app, Response)
response = client.get("/")
expected_status = 200 if status_code is None else status_code
assert response.status_code == expected_status
if should_capture:
(event,) = events
assert event["exception"]["values"][0]["type"] == "HTTPResponse"
else:
assert not events
def test_failed_request_status_codes_non_http_exception(sentry_init, capture_events):
"""
If an exception, which is not an instance of HTTPResponse, is raised, it should be captured, even if
failed_request_status_codes is empty.
"""
sentry_init(integrations=[BottleIntegration(failed_request_status_codes=set())])
events = capture_events()
app = Bottle()
@app.route("/")
def handle():
1 / 0
client = Client(app, Response)
try:
client.get("/")
except ZeroDivisionError:
pass
(event,) = events
assert event["exception"]["values"][0]["type"] == "ZeroDivisionError"
def test_span_streaming_basic(sentry_init, capture_items):
sentry_init(
integrations=[BottleIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("span")
app = Bottle()
@app.route("/message")
def hi():
return "ok"
client = Client(app)
client.get("/message")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 1
segment = spans[0]
# Segment span (root, created by WSGI middleware)
assert segment["is_segment"] is True
assert "parent_span_id" not in segment
assert segment["status"] == "ok"
assert segment["attributes"]["sentry.op"] == "http.server"
assert segment["attributes"]["sentry.origin"] == "auto.http.bottle"
assert segment["attributes"]["http.request.method"] == "GET"
assert segment["attributes"]["http.response.status_code"] == 200
assert segment["name"].endswith("hi")
@pytest.mark.parametrize(
"url,transaction_style,expected_name,expected_source",
[
("/message", "endpoint", "hi", "component"),
("/message", "url", "/message", "route"),
("/message/123456", "url", "/message/", "route"),
("/message-named-route", "endpoint", "hi", "component"),
],
)
def test_span_streaming_transaction_style(
sentry_init,
capture_items,
url,
transaction_style,
expected_name,
expected_source,
):
sentry_init(
integrations=[BottleIntegration(transaction_style=transaction_style)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("span")
app = Bottle()
@app.route("/message")
def hi():
return "ok"
@app.route("/message/")
def hi_with_id(message_id):
return "ok"
@app.route("/message-named-route", name="hi")
def named_hi():
return "ok"
client = Client(app)
client.get(url)
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 1
segment = spans[0]
assert segment["is_segment"] is True
assert segment["name"].endswith(expected_name)
assert segment["attributes"]["sentry.span.source"] == expected_source
def test_span_streaming_with_error(sentry_init, capture_items):
sentry_init(
integrations=[BottleIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("event", "span")
app = Bottle()
@app.route("/error")
def error():
1 / 0
client = Client(app)
try:
client.get("/error")
except ZeroDivisionError:
pass
sentry_sdk.flush()
events = [item.payload for item in items if item.type == "event"]
spans = [item.payload for item in items if item.type == "span"]
assert len(events) == 1
assert len(spans) == 1
error_event = events[0]
segment = spans[0]
# Confirm the same trace is shared
assert segment["trace_id"] == error_event["contexts"]["trace"]["trace_id"]
# Span hierarchy
assert segment["is_segment"] is True
assert "parent_span_id" not in segment
# Error event span_id points to the segment span (where the exception was raised)
assert error_event["contexts"]["trace"]["span_id"] == segment["span_id"]
# Span status
assert segment["status"] == "error"
# Bottle mechanism on the error event
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "bottle"
assert error_event["exception"]["values"][0]["mechanism"]["handled"] is False
@pytest.mark.parametrize(
"status_code,expected_span_status",
[
(200, "ok"),
(404, "error"),
(500, "error"),
],
)
def test_span_streaming_http_error_status(
sentry_init,
capture_items,
status_code,
expected_span_status,
):
sentry_init(
integrations=[BottleIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("span")
app = Bottle()
@app.route("/")
def handle():
return HTTPResponse(status=status_code, body="response")
client = Client(app)
client.get("/")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 1
segment = spans[0]
assert segment["is_segment"] is True
assert segment["status"] == expected_span_status
assert segment["attributes"]["http.response.status_code"] == status_code
@pytest.mark.parametrize("raise_error", [True, False])
@pytest.mark.parametrize(
("integration_kwargs", "status_code", "should_capture"),
(
({}, 500, True),
({}, 400, False),
({"failed_request_status_codes": set()}, 500, False),
({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True),
({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False),
),
)
def test_span_streaming_failed_request_status_codes(
sentry_init,
capture_items,
integration_kwargs,
status_code,
should_capture,
raise_error,
):
sentry_init(
integrations=[BottleIntegration(**integration_kwargs)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("event", "span")
app = Bottle()
@app.route("/")
def handle():
response = HTTPResponse(status=status_code)
if raise_error:
raise response
return response
client = Client(app, Response)
client.get("/")
sentry_sdk.flush()
events = [item.payload for item in items if item.type == "event"]
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
segment = spans[0]
assert segment["is_segment"] is True
if should_capture:
assert len(events) == 1
assert events[0]["exception"]["values"][0]["type"] == "HTTPResponse"
assert events[0]["exception"]["values"][0]["mechanism"]["handled"] is True
else:
assert len(events) == 0
sentry-python-2.64.0/tests/integrations/celery/ 0000775 0000000 0000000 00000000000 15220673775 0021617 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/celery/__init__.py 0000664 0000000 0000000 00000000055 15220673775 0023730 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("celery")
sentry-python-2.64.0/tests/integrations/celery/integration_tests/ 0000775 0000000 0000000 00000000000 15220673775 0025364 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/celery/integration_tests/__init__.py 0000664 0000000 0000000 00000002733 15220673775 0027502 0 ustar 00root root 0000000 0000000 import os
import signal
import tempfile
import threading
import time
from celery.beat import Scheduler
from sentry_sdk.utils import logger
class ImmediateScheduler(Scheduler):
"""
A custom scheduler that starts tasks immediately after starting Celery beat.
"""
def setup_schedule(self):
super().setup_schedule()
for _, entry in self.schedule.items():
self.apply_entry(entry)
def tick(self):
# Override tick to prevent the normal schedule cycle
return 1
def kill_beat(beat_pid_file, delay_seconds=1):
"""
Terminates Celery Beat after the given `delay_seconds`.
"""
logger.info("Starting Celery Beat killer...")
time.sleep(delay_seconds)
pid = int(open(beat_pid_file, "r").read())
logger.info("Terminating Celery Beat...")
os.kill(pid, signal.SIGTERM)
def run_beat(celery_app, runtime_seconds=1, loglevel="warning", quiet=True):
"""
Run Celery Beat that immediately starts tasks.
The Celery Beat instance is automatically terminated after `runtime_seconds`.
"""
logger.info("Starting Celery Beat...")
pid_file = os.path.join(tempfile.mkdtemp(), f"celery-beat-{os.getpid()}.pid")
t = threading.Thread(
target=kill_beat,
args=(pid_file,),
kwargs={"delay_seconds": runtime_seconds},
)
t.start()
beat_instance = celery_app.Beat(
loglevel=loglevel,
quiet=quiet,
pidfile=pid_file,
)
beat_instance.run()
sentry-python-2.64.0/tests/integrations/celery/integration_tests/test_celery_beat_cron_monitoring.py0000664 0000000 0000000 00000011225 15220673775 0034542 0 ustar 00root root 0000000 0000000 import os
import sys
import pytest
from celery.contrib.testing.worker import start_worker
from sentry_sdk.utils import logger
from tests.integrations.celery.integration_tests import run_beat
REDIS_SERVER = (
f"redis://{os.environ.get('SENTRY_PYTHON_TEST_REDIS_HOST', '127.0.0.1')}:6379"
)
REDIS_DB = 15
@pytest.fixture()
def celery_config():
return {
"worker_concurrency": 1,
"broker_url": f"{REDIS_SERVER}/{REDIS_DB}",
"result_backend": f"{REDIS_SERVER}/{REDIS_DB}",
"beat_scheduler": "tests.integrations.celery.integration_tests:ImmediateScheduler",
"task_always_eager": False,
"task_create_missing_queues": True,
"task_default_queue": f"queue_{os.getpid()}",
}
@pytest.fixture
def celery_init(sentry_init, celery_config):
"""
Create a Sentry instrumented Celery app.
"""
from celery import Celery
from sentry_sdk.integrations.celery import CeleryIntegration
def inner(propagate_traces=True, monitor_beat_tasks=False, **kwargs):
sentry_init(
integrations=[
CeleryIntegration(
propagate_traces=propagate_traces,
monitor_beat_tasks=monitor_beat_tasks,
)
],
**kwargs,
)
app = Celery("tasks")
app.conf.update(celery_config)
return app
return inner
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
@pytest.mark.forked
def test_explanation(celery_init, capture_envelopes):
"""
This is a dummy test for explaining how to test using Celery Beat
"""
# First initialize a Celery app.
# You can give the options of CeleryIntegrations
# and the options for `sentry_dks.init` as keyword arguments.
# See the celery_init fixture for details.
app = celery_init(
monitor_beat_tasks=True,
)
# Capture envelopes.
envelopes = capture_envelopes()
# Define the task you want to run
@app.task
def test_task():
logger.info("Running test_task")
# Add the task to the beat schedule
app.add_periodic_task(60.0, test_task.s(), name="success_from_beat")
# Start a Celery worker
with start_worker(app, perform_ping_check=False):
# And start a Celery Beat instance
# This Celery Beat will start the task above immediately
# after start for the first time
# By default Celery Beat is terminated after 1 second.
# See `run_beat` function on how to change this.
run_beat(app)
# After the Celery Beat is terminated, you can check the envelopes
assert len(envelopes) >= 0
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
@pytest.mark.forked
def test_beat_task_crons_success(celery_init, capture_envelopes):
app = celery_init(
monitor_beat_tasks=True,
)
envelopes = capture_envelopes()
@app.task
def test_task():
logger.info("Running test_task")
app.add_periodic_task(60.0, test_task.s(), name="success_from_beat")
with start_worker(app, perform_ping_check=False):
run_beat(app)
assert len(envelopes) == 2
(envelop_in_progress, envelope_ok) = envelopes
assert envelop_in_progress.items[0].headers["type"] == "check_in"
check_in = envelop_in_progress.items[0].payload.json
assert check_in["type"] == "check_in"
assert check_in["monitor_slug"] == "success_from_beat"
assert check_in["status"] == "in_progress"
assert envelope_ok.items[0].headers["type"] == "check_in"
check_in = envelope_ok.items[0].payload.json
assert check_in["type"] == "check_in"
assert check_in["monitor_slug"] == "success_from_beat"
assert check_in["status"] == "ok"
@pytest.mark.skipif(sys.version_info < (3, 7), reason="Requires Python 3.7+")
@pytest.mark.forked
def test_beat_task_crons_error(celery_init, capture_envelopes):
app = celery_init(
monitor_beat_tasks=True,
)
envelopes = capture_envelopes()
@app.task
def test_task():
logger.info("Running test_task")
1 / 0
app.add_periodic_task(60.0, test_task.s(), name="failure_from_beat")
with start_worker(app, perform_ping_check=False):
run_beat(app)
envelop_in_progress = envelopes[0]
envelope_error = envelopes[-1]
check_in = envelop_in_progress.items[0].payload.json
assert check_in["type"] == "check_in"
assert check_in["monitor_slug"] == "failure_from_beat"
assert check_in["status"] == "in_progress"
check_in = envelope_error.items[0].payload.json
assert check_in["type"] == "check_in"
assert check_in["monitor_slug"] == "failure_from_beat"
assert check_in["status"] == "error"
sentry-python-2.64.0/tests/integrations/celery/test_celery.py 0000664 0000000 0000000 00000114336 15220673775 0024523 0 ustar 00root root 0000000 0000000 import os
import threading
from unittest import mock
import kombu
import pytest
from celery import VERSION, Celery
from celery.bin import worker
import sentry_sdk
import sentry_sdk.traces
from sentry_sdk import get_current_span, start_transaction
from sentry_sdk.integrations.celery import (
CeleryIntegration,
_wrap_task_run,
)
from sentry_sdk.integrations.celery.beat import _get_headers
from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
from tests.conftest import ApproxDict
@pytest.fixture
def connect_signal(request):
def inner(signal, f):
signal.connect(f)
request.addfinalizer(lambda: signal.disconnect(f))
return inner
@pytest.fixture
def init_celery(sentry_init, request):
def inner(
propagate_traces=True,
backend="always_eager",
monitor_beat_tasks=False,
**kwargs,
):
sentry_init(
integrations=[
CeleryIntegration(
propagate_traces=propagate_traces,
monitor_beat_tasks=monitor_beat_tasks,
)
],
**kwargs,
)
celery = Celery(__name__)
if backend == "always_eager":
if VERSION < (4,):
celery.conf.CELERY_ALWAYS_EAGER = True
else:
celery.conf.task_always_eager = True
elif backend == "redis":
# broken on celery 3
if VERSION < (4,):
pytest.skip("Redis backend broken for some reason")
# this backend requires capture_events_forksafe
celery.conf.worker_max_tasks_per_child = 1
celery.conf.worker_concurrency = 1
redis_url = f"redis://{os.environ.get('SENTRY_PYTHON_TEST_REDIS_HOST', '127.0.0.1')}:6379"
celery.conf.broker_url = redis_url
celery.conf.result_backend = redis_url
celery.conf.task_always_eager = False
# Once we drop celery 3 we can use the celery_worker fixture
if VERSION < (5,):
worker_fn = worker.worker(app=celery).run
else:
from celery.bin.base import CLIContext
worker_fn = lambda: worker.worker(
obj=CLIContext(app=celery, no_color=True, workdir=".", quiet=False),
args=[],
)
worker_thread = threading.Thread(target=worker_fn)
worker_thread.daemon = True
worker_thread.start()
else:
raise ValueError(backend)
return celery
return inner
@pytest.fixture
def celery(init_celery):
return init_celery()
@pytest.fixture(
params=[
lambda task, x, y: (
task.delay(x, y),
{"args": [x, y], "kwargs": {}},
),
lambda task, x, y: (
task.apply_async((x, y)),
{"args": [x, y], "kwargs": {}},
),
lambda task, x, y: (
task.apply_async(args=(x, y)),
{"args": [x, y], "kwargs": {}},
),
lambda task, x, y: (
task.apply_async(kwargs=dict(x=x, y=y)),
{"args": [], "kwargs": {"x": x, "y": y}},
),
]
)
def celery_invocation(request):
"""
Invokes a task in multiple ways Celery allows you to (testing our apply_async monkeypatch).
Currently limited to a task signature of the form foo(x, y)
"""
return request.param
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("send_default_pii", [True, False])
def test_simple_with_performance(
capture_events,
capture_items,
init_celery,
celery_invocation,
span_streaming,
send_default_pii,
):
celery = init_celery(
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task(name="dummy_task")
def dummy_task(x, y):
foo = 42 # noqa
return x / y
if span_streaming:
items = capture_items("event", "span")
with sentry_sdk.traces.start_span(name="span") as span:
celery_invocation(dummy_task, 1, 2)
_, expected_context = celery_invocation(dummy_task, 1, 0)
sentry_sdk.flush()
error_event = next(item.payload for item in items if item.type == "event")
assert error_event["contexts"]["trace"]["trace_id"] == span.trace_id
assert error_event["contexts"]["trace"]["span_id"] != span.span_id
else:
events = capture_events()
with start_transaction(op="unit test transaction") as transaction:
celery_invocation(dummy_task, 1, 2)
_, expected_context = celery_invocation(dummy_task, 1, 0)
(_, error_event, _, _) = events
assert error_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert error_event["contexts"]["trace"]["span_id"] != transaction.span_id
assert error_event["transaction"] == "dummy_task"
assert "celery_task_id" in error_event["tags"]
if send_default_pii:
assert error_event["extra"]["celery-job"] == dict(
task_name="dummy_task", **expected_context
)
else:
assert error_event["extra"]["celery-job"] == {
"task_name": "dummy_task",
"args": SENSITIVE_DATA_SUBSTITUTE,
"kwargs": SENSITIVE_DATA_SUBSTITUTE,
}
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "celery"
assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42"
@pytest.mark.parametrize("send_default_pii", [True, False])
def test_simple_without_performance(
capture_events, init_celery, celery_invocation, send_default_pii
):
celery = init_celery(traces_sample_rate=None, send_default_pii=send_default_pii)
events = capture_events()
@celery.task(name="dummy_task")
def dummy_task(x, y):
foo = 42 # noqa
return x / y
scope = sentry_sdk.get_isolation_scope()
celery_invocation(dummy_task, 1, 2)
_, expected_context = celery_invocation(dummy_task, 1, 0)
(error_event,) = events
assert (
error_event["contexts"]["trace"]["trace_id"]
== scope._propagation_context.trace_id
)
assert (
error_event["contexts"]["trace"]["span_id"]
!= scope._propagation_context.span_id
)
assert error_event["transaction"] == "dummy_task"
assert "celery_task_id" in error_event["tags"]
if send_default_pii:
assert error_event["extra"]["celery-job"] == dict(
task_name="dummy_task", **expected_context
)
else:
assert error_event["extra"]["celery-job"] == {
"task_name": "dummy_task",
"args": SENSITIVE_DATA_SUBSTITUTE,
"kwargs": SENSITIVE_DATA_SUBSTITUTE,
}
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "celery"
assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("task_fails", [True, False], ids=["error", "success"])
def test_transaction_events(
capture_events,
capture_items,
init_celery,
celery_invocation,
task_fails,
span_streaming,
):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@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)
sentry_sdk.flush()
if span_streaming:
items = capture_items("event", "span")
with sentry_sdk.traces.start_span(name="submission") as span:
celery_invocation(dummy_task, 1, 0 if task_fails else 1)
sentry_sdk.flush()
if task_fails:
error_event = items.pop(0).payload
assert error_event["contexts"]["trace"]["trace_id"] == span.trace_id
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
process_span, execution_span, submit_span, submission_span = [
item.payload for item in items
]
assert execution_span["name"] == "dummy_task"
assert execution_span["is_segment"] is True
assert execution_span["attributes"]["sentry.span.source"] == "task"
assert execution_span["trace_id"] == span.trace_id
if task_fails:
assert execution_span["status"] == "error"
else:
assert execution_span["status"] == "ok"
assert process_span["name"] == "dummy_task"
assert process_span["trace_id"] == span.trace_id
assert process_span["attributes"]["sentry.op"] == "queue.process"
assert process_span["parent_span_id"] == execution_span["span_id"]
assert submission_span["name"] == "submission"
assert submission_span["is_segment"] is True
assert submit_span["name"] == "dummy_task"
assert submit_span["attributes"]["sentry.op"] == "queue.submit.celery"
assert submit_span["attributes"]["sentry.origin"] == "auto.queue.celery"
assert (
submit_span["parent_span_id"] == submission_span["span_id"] == span.span_id
)
assert submit_span["trace_id"] == span.trace_id
else:
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 execution_event["transaction_info"] == {"source": "task"}
assert submission_event["transaction"] == "submission"
assert submission_event["transaction_info"] == {"source": "custom"}
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 len(execution_event["spans"]) == 1
assert (
execution_event["spans"][0].items()
>= {
"trace_id": str(transaction.trace_id),
"same_process_as_parent": True,
"op": "queue.process",
"description": "dummy_task",
"data": ApproxDict(),
}.items()
)
assert submission_event["spans"] == [
{
"data": ApproxDict(),
"description": "dummy_task",
"op": "queue.submit.celery",
"origin": "auto.queue.celery",
"parent_span_id": submission_event["contexts"]["trace"]["span_id"],
"same_process_as_parent": True,
"span_id": submission_event["spans"][0]["span_id"],
"start_timestamp": submission_event["spans"][0]["start_timestamp"],
"timestamp": submission_event["spans"][0]["timestamp"],
"trace_id": str(transaction.trace_id),
}
]
def test_no_double_patching(celery):
"""Ensure that Celery tasks are only patched once to prevent stack overflows.
We used to have a bug in the Celery integration where its monkeypatching
was repeated for every task invocation, leading to stackoverflows.
See https://github.com/getsentry/sentry-python/issues/265
"""
@celery.task(name="dummy_task")
def dummy_task():
return 42
# Initially, the task should not be marked as patched
assert not hasattr(dummy_task, "_sentry_is_patched")
# First invocation should trigger patching
result1 = dummy_task.delay()
assert result1.get() == 42
assert getattr(dummy_task, "_sentry_is_patched", False) is True
patched_run = dummy_task.run
# Second invocation should not re-patch
result2 = dummy_task.delay()
assert result2.get() == 42
assert dummy_task.run is patched_run
assert getattr(dummy_task, "_sentry_is_patched", False) is True
def test_simple_no_propagation(capture_events, init_celery):
celery = init_celery(propagate_traces=False)
events = capture_events()
@celery.task(name="dummy_task")
def dummy_task():
1 / 0
with start_transaction() as transaction:
dummy_task.delay()
(event,) = events
assert event["contexts"]["trace"]["trace_id"] != transaction.trace_id
assert event["transaction"] == "dummy_task"
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
def test_ignore_expected(capture_events, celery):
events = capture_events()
@celery.task(name="dummy_task", throws=(ZeroDivisionError,))
def dummy_task(x, y):
return x / y
dummy_task.delay(1, 2)
dummy_task.delay(1, 0)
assert not events
@pytest.mark.xfail(
(4, 2, 0) <= VERSION < (4, 4, 3),
strict=True,
reason="https://github.com/celery/celery/issues/4661",
)
def test_retry(celery, capture_events):
events = capture_events()
failures = [True, True, False]
runs = []
@celery.task(name="dummy_task", bind=True)
def dummy_task(self):
runs.append(1)
try:
if failures.pop(0):
1 / 0
except Exception as exc:
self.retry(max_retries=2, exc=exc)
dummy_task.delay()
assert len(runs) == 3
assert not events
failures = [True, True, True]
runs = []
dummy_task.delay()
assert len(runs) == 3
(event,) = events
exceptions = event["exception"]["values"]
for e in exceptions:
assert e["type"] == "ZeroDivisionError"
@pytest.mark.skip(
reason="This test is hanging when running test with `tox --parallel auto`. TODO: Figure out why and fix it!"
)
@pytest.mark.forked
def test_redis_backend_trace_propagation(init_celery, capture_events_forksafe):
celery = init_celery(traces_sample_rate=1.0, backend="redis")
events = capture_events_forksafe()
runs = []
@celery.task(name="dummy_task", bind=True)
def dummy_task(self):
runs.append(1)
1 / 0
with start_transaction(name="submit_celery"):
# Curious: Cannot use delay() here or py2.7-celery-4.2 crashes
res = dummy_task.apply_async()
with pytest.raises(Exception): # noqa: B017
# Celery 4.1 raises a gibberish exception
res.wait()
# if this is nonempty, the worker never really forked
assert not runs
submit_transaction = events.read_event()
assert submit_transaction["type"] == "transaction"
assert submit_transaction["transaction"] == "submit_celery"
assert len(submit_transaction["spans"]), (
4
) # Because redis integration was auto enabled
span = submit_transaction["spans"][0]
assert span["op"] == "queue.submit.celery"
assert span["description"] == "dummy_task"
event = events.read_event()
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
transaction = events.read_event()
assert (
transaction["contexts"]["trace"]["trace_id"]
== event["contexts"]["trace"]["trace_id"]
== submit_transaction["contexts"]["trace"]["trace_id"]
)
events.read_flush()
# if this is nonempty, the worker never really forked
assert not runs
@pytest.mark.forked
@pytest.mark.parametrize("newrelic_order", ["sentry_first", "sentry_last"])
def test_newrelic_interference(init_celery, newrelic_order, celery_invocation):
def instrument_newrelic():
try:
# older newrelic versions
import celery.app.trace as celery_trace_module
from newrelic.hooks.application_celery import (
instrument_celery_execute_trace,
)
assert hasattr(celery_trace_module, "build_tracer")
instrument_celery_execute_trace(celery_trace_module)
except ImportError:
# newer newrelic versions
import celery.app as celery_app_module
from newrelic.hooks.application_celery import instrument_celery_app_base
assert hasattr(celery_app_module, "Celery")
assert hasattr(celery_app_module.Celery, "send_task")
instrument_celery_app_base(celery_app_module)
if newrelic_order == "sentry_first":
celery = init_celery()
instrument_newrelic()
elif newrelic_order == "sentry_last":
instrument_newrelic()
celery = init_celery()
else:
raise ValueError(newrelic_order)
@celery.task(name="dummy_task", bind=True)
def dummy_task(self, x, y):
return x / y
assert dummy_task.apply(kwargs={"x": 1, "y": 1}).wait() == 1
assert celery_invocation(dummy_task, 1, 1)[0].wait() == 1
@pytest.mark.parametrize("span_streaming", [True, False])
def test_traces_sampler_gets_task_info_in_sampling_context(
span_streaming,
init_celery,
celery_invocation,
DictionaryContaining, # noqa:N803
):
traces_sampler = mock.Mock(return_value=1.0)
celery = init_celery(
traces_sampler=traces_sampler,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task(name="dog_walk")
def walk_dogs(x, y):
dogs, route = x
num_loops = y
return dogs, route, num_loops
_, args_kwargs = celery_invocation(
walk_dogs, [["Maisey", "Charlie", "Bodhi", "Cory"], "Dog park round trip"], 1
)
traces_sampler.assert_any_call(
# depending on the iteration of celery_invocation, the data might be
# passed as args or as kwargs, so make this generic
DictionaryContaining({"celery_job": dict(task="dog_walk", **args_kwargs)})
)
def test_abstract_task(capture_events, celery, celery_invocation):
events = capture_events()
class AbstractTask(celery.Task):
abstract = True
def __call__(self, *args, **kwargs):
try:
return self.run(*args, **kwargs)
except ZeroDivisionError:
return None
@celery.task(name="dummy_task", base=AbstractTask)
def dummy_task(x, y):
return x / y
with start_transaction():
celery_invocation(dummy_task, 1, 0)
assert not events
def test_task_headers(celery):
"""
Test that the headers set in the Celery Beat auto-instrumentation are passed to the celery signal handlers
"""
sentry_crons_setup = {
"sentry-monitor-slug": "some-slug",
"sentry-monitor-config": {"some": "config"},
"sentry-monitor-check-in-id": "123abc",
}
@celery.task(name="dummy_task", bind=True)
def dummy_task(self, x, y):
return _get_headers(self)
# This is how the Celery Beat auto-instrumentation starts a task
# in the monkey patched version of `apply_async`
# in `sentry_sdk/integrations/celery.py::_wrap_apply_async()`
result = dummy_task.apply_async(args=(1, 0), headers=sentry_crons_setup)
expected_headers = sentry_crons_setup.copy()
# Newly added headers
expected_headers["sentry-trace"] = mock.ANY
expected_headers["baggage"] = mock.ANY
expected_headers["sentry-task-enqueued-time"] = mock.ANY
assert result.get() == expected_headers
def test_baggage_propagation(init_celery):
celery = init_celery(traces_sample_rate=1.0, release="abcdef")
@celery.task(name="dummy_task", bind=True)
def dummy_task(self, x, y):
return _get_headers(self)
# patch random.randrange to return a predictable sample_rand value
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
with start_transaction() as transaction:
result = dummy_task.apply_async(
args=(1, 0),
headers={"baggage": "custom=value"},
).get()
assert sorted(result["baggage"].split(",")) == sorted(
[
"sentry-release=abcdef",
"sentry-trace_id={}".format(transaction.trace_id),
"sentry-environment=production",
"sentry-sample_rand=0.500000",
"sentry-sample_rate=1.0",
"sentry-sampled=true",
"custom=value",
]
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_sentry_propagate_traces_override(span_streaming, init_celery):
"""
Test if the `sentry-propagate-traces` header given to `apply_async`
overrides the `propagate_traces` parameter in the integration constructor.
"""
celery = init_celery(
propagate_traces=True,
traces_sample_rate=1.0,
release="abcdef",
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task(name="dummy_task", bind=True)
def dummy_task(self, message):
trace_id = (
sentry_sdk.traces.get_current_span().trace_id
if span_streaming
else get_current_span().trace_id
)
return trace_id
if span_streaming:
with sentry_sdk.traces.start_span(name="parent") as span:
parent_trace_id = span.trace_id
# should propagate trace
task_trace_id = dummy_task.apply_async(
args=("some message",),
).get()
assert parent_trace_id == task_trace_id
# should NOT propagate trace
task_trace_id = dummy_task.apply_async(
args=("another message",),
headers={"sentry-propagate-traces": False},
).get()
assert parent_trace_id != task_trace_id
else:
with start_transaction() as transaction:
transaction_trace_id = transaction.trace_id
# should propagate trace
task_trace_id = dummy_task.apply_async(
args=("some message",),
).get()
assert transaction_trace_id == task_trace_id
# should NOT propagate trace
task_trace_id = dummy_task.apply_async(
args=("another message",),
headers={"sentry-propagate-traces": False},
).get()
assert transaction_trace_id != task_trace_id
def test_apply_async_manually_span(sentry_init):
sentry_init(
integrations=[CeleryIntegration()],
)
def dummy_function(*args, **kwargs):
headers = kwargs.get("headers")
assert "sentry-trace" in headers
assert "baggage" in headers
wrapped = _wrap_task_run(dummy_function)
wrapped(mock.MagicMock(), (), headers={})
def test_apply_async_no_args(init_celery):
celery = init_celery()
@celery.task
def example_task():
return "success"
try:
result = example_task.apply_async(None, {})
except TypeError:
pytest.fail("Calling `apply_async` without arguments raised a TypeError")
assert result.get() == "success"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("routing_key", ("celery", "custom"))
@mock.patch("celery.app.task.Task.request")
def test_messaging_destination_name_default_exchange(
mock_request,
routing_key,
span_streaming,
init_celery,
capture_events,
capture_items,
):
celery_app = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
mock_request.delivery_info = {"routing_key": routing_key, "exchange": ""}
@celery_app.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert (
process_span.payload["attributes"]["messaging.destination.name"]
== routing_key
)
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.destination.name"] == routing_key
@pytest.mark.parametrize("span_streaming", [True, False])
@mock.patch("celery.app.task.Task.request")
def test_messaging_destination_name_nondefault_exchange(
mock_request, span_streaming, init_celery, capture_events, capture_items
):
"""
Currently, we only capture the routing key as the messaging.destination.name when
we are using the default exchange (""). This is because the default exchange ensures
that the routing key is the queue name. Other exchanges may not guarantee this
behavior.
"""
celery_app = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
mock_request.delivery_info = {"routing_key": "celery", "exchange": "custom"}
@celery_app.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert "messaging.destination.name" not in process_span.payload["attributes"]
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.destination.name" not in span["data"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_messaging_id(span_streaming, init_celery, capture_events, capture_items):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task
def example_task(): ...
if span_streaming:
items = capture_items("span")
example_task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert "messaging.message.id" in process_span.payload["attributes"]
else:
events = capture_events()
example_task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.message.id" in span["data"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_retry_count_zero(span_streaming, init_celery, capture_events, capture_items):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert process_span.payload["attributes"]["messaging.message.retry.count"] == 0
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.message.retry.count"] == 0
@pytest.mark.parametrize("span_streaming", [True, False])
@mock.patch("celery.app.task.Task.request")
def test_retry_count_nonzero(
mock_request, span_streaming, init_celery, capture_events, capture_items
):
mock_request.retries = 3
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert process_span.payload["attributes"]["messaging.message.retry.count"] == 3
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.message.retry.count"] == 3
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("system", ("redis", "amqp"))
def test_messaging_system(
system, span_streaming, init_celery, capture_events, capture_items
):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
# Does not need to be a real URL, since we use always eager
celery.conf.broker_url = f"{system}://example.com" # noqa: E231
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert process_span.payload["attributes"]["messaging.system"] == system
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.system"] == system
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("system", ("amqp", "redis"))
def test_producer_span_data(
system, span_streaming, monkeypatch, sentry_init, capture_events, capture_items
):
old_publish = kombu.messaging.Producer._publish
def publish(*args, **kwargs):
pass
monkeypatch.setattr(kombu.messaging.Producer, "_publish", publish)
sentry_init(
integrations=[CeleryIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
celery = Celery(__name__, broker=f"{system}://example.com") # noqa: E231
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="producer test"):
task.apply_async()
sentry_sdk.flush()
span_items = [item.payload for item in items]
publish_span = next(
s for s in span_items if s["attributes"].get("sentry.op") == "queue.publish"
)
assert publish_span["attributes"]["messaging.system"] == system
assert publish_span["attributes"]["messaging.destination.name"] == "celery"
assert "messaging.message.id" in publish_span["attributes"]
assert publish_span["attributes"]["messaging.message.retry.count"] == 0
else:
events = capture_events()
with start_transaction():
task.apply_async()
(event,) = events
span = next(span for span in event["spans"] if span["op"] == "queue.publish")
assert span["data"]["messaging.system"] == system
assert span["data"]["messaging.destination.name"] == "celery"
assert "messaging.message.id" in span["data"]
assert span["data"]["messaging.message.retry.count"] == 0
monkeypatch.setattr(kombu.messaging.Producer, "_publish", old_publish)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_receive_latency(span_streaming, init_celery, capture_events, capture_items):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, _execution_span = items
assert "messaging.message.receive.latency" in process_span.payload["attributes"]
assert (
process_span.payload["attributes"]["messaging.message.receive.latency"] > 0
)
else:
events = capture_events()
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.message.receive.latency" in span["data"]
assert span["data"]["messaging.message.receive.latency"] > 0
@pytest.mark.parametrize("span_streaming", [True, False])
def tests_span_origin_consumer(
span_streaming, init_celery, capture_events, capture_items
):
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
celery.conf.broker_url = "redis://example.com" # noqa: E231
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
task.apply_async()
sentry_sdk.flush()
process_span, execution_span = items
assert (
execution_span.payload["attributes"]["sentry.origin"] == "auto.queue.celery"
)
assert (
process_span.payload["attributes"]["sentry.origin"] == "auto.queue.celery"
)
else:
events = capture_events()
task.apply_async()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.queue.celery"
assert event["spans"][0]["origin"] == "auto.queue.celery"
@pytest.mark.parametrize("span_streaming", [True, False])
def tests_span_origin_producer(
span_streaming, monkeypatch, sentry_init, capture_events, capture_items
):
old_publish = kombu.messaging.Producer._publish
def publish(*args, **kwargs):
pass
monkeypatch.setattr(kombu.messaging.Producer, "_publish", publish)
sentry_init(
integrations=[CeleryIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
celery = Celery(__name__, broker="redis://example.com") # noqa: E231
@celery.task()
def task(): ...
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
task.apply_async()
sentry_sdk.flush()
parent = items.pop(-1).payload
assert parent["name"] == "custom parent"
assert parent["attributes"]["sentry.origin"] == "manual"
for item in items:
assert item.payload["attributes"]["sentry.origin"] == "auto.queue.celery"
else:
events = capture_events()
with start_transaction(name="custom_transaction"):
task.apply_async()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.queue.celery"
monkeypatch.setattr(kombu.messaging.Producer, "_publish", old_publish)
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.forked
@mock.patch("celery.Celery.send_task")
def test_send_task_wrapped(
patched_send_task,
span_streaming,
sentry_init,
capture_events,
capture_items,
reset_integrations,
):
sentry_init(
integrations=[CeleryIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
celery = Celery(__name__, broker="redis://example.com") # noqa: E231
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent") as outer_span:
celery.send_task(
"very_creative_task_name", args=(1, 2), kwargs={"foo": "bar"}
)
sentry_sdk.flush()
else:
events = capture_events()
with sentry_sdk.start_transaction(name="custom_transaction"):
celery.send_task(
"very_creative_task_name", args=(1, 2), kwargs={"foo": "bar"}
)
(call,) = patched_send_task.call_args_list # We should have exactly one call
(args, kwargs) = call
assert args == (celery, "very_creative_task_name")
assert kwargs["args"] == (1, 2)
assert kwargs["kwargs"] == {"foo": "bar"}
assert set(kwargs["headers"].keys()) == {
"sentry-task-enqueued-time",
"sentry-trace",
"baggage",
"headers",
}
assert set(kwargs["headers"]["headers"].keys()) == {
"sentry-trace",
"baggage",
"sentry-task-enqueued-time",
}
assert (
kwargs["headers"]["sentry-trace"]
== kwargs["headers"]["headers"]["sentry-trace"]
)
if span_streaming:
submit_span, outer = [item.payload for item in items]
assert outer["name"] == "custom parent"
assert outer["is_segment"] is True
assert submit_span["name"] == "very_creative_task_name"
assert submit_span["attributes"]["sentry.op"] == "queue.submit.celery"
assert submit_span["trace_id"] == outer_span.trace_id
assert (
submit_span["trace_id"] == kwargs["headers"]["sentry-trace"].split("-")[0]
)
else:
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "custom_transaction"
(span,) = event["spans"]
assert span["description"] == "very_creative_task_name"
assert span["op"] == "queue.submit.celery"
assert span["trace_id"] == kwargs["headers"]["sentry-trace"].split("-")[0]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_user_custom_headers_accessible_in_task(span_streaming, init_celery):
"""
Regression test for https://github.com/getsentry/sentry-python/issues/5566
User-provided custom headers passed to apply_async() must be accessible
via task.request.headers on the worker side.
"""
celery = init_celery(
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
@celery.task(name="custom_headers_task", bind=True)
def custom_headers_task(self):
return dict(self.request.headers or {})
custom_headers = {
"my_custom_key": "my_value",
"correlation_id": "abc-123",
"tenant_id": "tenant-42",
}
if span_streaming:
with sentry_sdk.traces.start_span(name="test"):
result = custom_headers_task.apply_async(headers=custom_headers)
else:
with start_transaction(name="test"):
result = custom_headers_task.apply_async(headers=custom_headers)
received_headers = result.get()
for key, value in custom_headers.items():
assert received_headers.get(key) == value, (
f"Custom header {key!r} not found in task.request.headers"
)
@pytest.mark.skip(reason="placeholder so that forked test does not come last")
def test_placeholder():
"""Forked tests must not come last in the module.
See https://github.com/pytest-dev/pytest-forked/issues/67#issuecomment-1964718720.
"""
pass
sentry-python-2.64.0/tests/integrations/celery/test_celery_beat_crons.py 0000664 0000000 0000000 00000037346 15220673775 0026727 0 ustar 00root root 0000000 0000000 import datetime
from unittest import mock
from unittest.mock import MagicMock
import pytest
from celery.schedules import crontab, schedule
from sentry_sdk.crons import MonitorStatus
from sentry_sdk.integrations.celery.beat import (
_get_headers,
_get_monitor_config,
_patch_beat_apply_entry,
_patch_redbeat_apply_async,
crons_task_failure,
crons_task_retry,
crons_task_success,
)
from sentry_sdk.integrations.celery.utils import _get_humanized_interval
def test_get_headers():
fake_task = MagicMock()
fake_task.request = {
"bla": "blub",
"foo": "bar",
}
assert _get_headers(fake_task) == {}
fake_task.request.update(
{
"headers": {
"bla": "blub",
},
}
)
assert _get_headers(fake_task) == {"bla": "blub"}
fake_task.request.update(
{
"headers": {
"headers": {
"tri": "blub",
"bar": "baz",
},
"bla": "blub",
},
}
)
assert _get_headers(fake_task) == {"bla": "blub", "tri": "blub", "bar": "baz"}
@pytest.mark.parametrize(
"seconds, expected_tuple",
[
(0, (0, "second")),
(1, (1, "second")),
(0.00001, (0, "second")),
(59, (59, "second")),
(60, (1, "minute")),
(100, (1, "minute")),
(1000, (16, "minute")),
(10000, (2, "hour")),
(100000, (1, "day")),
(100000000, (1157, "day")),
],
)
def test_get_humanized_interval(seconds, expected_tuple):
assert _get_humanized_interval(seconds) == expected_tuple
def test_crons_task_success():
fake_task = MagicMock()
fake_task.request = {
"headers": {
"sentry-monitor-slug": "test123",
"sentry-monitor-check-in-id": "1234567890",
"sentry-monitor-start-timestamp-s": 200.1,
"sentry-monitor-config": {
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
"sentry-monitor-some-future-key": "some-future-value",
},
}
with mock.patch(
"sentry_sdk.integrations.celery.beat.capture_checkin"
) as mock_capture_checkin:
with mock.patch(
"sentry_sdk.integrations.celery.beat._now_seconds_since_epoch",
return_value=500.5,
):
crons_task_success(fake_task)
mock_capture_checkin.assert_called_once_with(
monitor_slug="test123",
monitor_config={
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
duration=300.4,
check_in_id="1234567890",
status=MonitorStatus.OK,
)
def test_crons_task_failure():
fake_task = MagicMock()
fake_task.request = {
"headers": {
"sentry-monitor-slug": "test123",
"sentry-monitor-check-in-id": "1234567890",
"sentry-monitor-start-timestamp-s": 200.1,
"sentry-monitor-config": {
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
"sentry-monitor-some-future-key": "some-future-value",
},
}
with mock.patch(
"sentry_sdk.integrations.celery.beat.capture_checkin"
) as mock_capture_checkin:
with mock.patch(
"sentry_sdk.integrations.celery.beat._now_seconds_since_epoch",
return_value=500.5,
):
crons_task_failure(fake_task)
mock_capture_checkin.assert_called_once_with(
monitor_slug="test123",
monitor_config={
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
duration=300.4,
check_in_id="1234567890",
status=MonitorStatus.ERROR,
)
def test_crons_task_retry():
fake_task = MagicMock()
fake_task.request = {
"headers": {
"sentry-monitor-slug": "test123",
"sentry-monitor-check-in-id": "1234567890",
"sentry-monitor-start-timestamp-s": 200.1,
"sentry-monitor-config": {
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
"sentry-monitor-some-future-key": "some-future-value",
},
}
with mock.patch(
"sentry_sdk.integrations.celery.beat.capture_checkin"
) as mock_capture_checkin:
with mock.patch(
"sentry_sdk.integrations.celery.beat._now_seconds_since_epoch",
return_value=500.5,
):
crons_task_retry(fake_task)
mock_capture_checkin.assert_called_once_with(
monitor_slug="test123",
monitor_config={
"schedule": {
"type": "interval",
"value": 3,
"unit": "day",
},
"timezone": "Europe/Vienna",
},
duration=300.4,
check_in_id="1234567890",
status=MonitorStatus.ERROR,
)
def test_get_monitor_config_crontab():
app = MagicMock()
app.timezone = "Europe/Vienna"
# schedule with the default timezone
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "crontab",
"value": "*/10 12 3 * *",
},
"timezone": "UTC", # the default because `crontab` does not know about the app
}
assert "unit" not in monitor_config["schedule"]
# schedule with the timezone from the app
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10", app=app)
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "crontab",
"value": "*/10 12 3 * *",
},
"timezone": "Europe/Vienna", # the timezone from the app
}
# schedule without a timezone, the celery integration will read the config from the app
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
celery_schedule.tz = None
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "crontab",
"value": "*/10 12 3 * *",
},
"timezone": "Europe/Vienna", # the timezone from the app
}
# schedule without a timezone, and an app without timezone, the celery integration will fall back to UTC
app = MagicMock()
app.timezone = None
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
celery_schedule.tz = None
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "crontab",
"value": "*/10 12 3 * *",
},
"timezone": "UTC", # default timezone from celery integration
}
def test_get_monitor_config_seconds():
app = MagicMock()
app.timezone = "Europe/Vienna"
celery_schedule = schedule(run_every=3) # seconds
with mock.patch("sentry_sdk.integrations.logger.warning") as mock_logger_warning:
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
mock_logger_warning.assert_called_with(
"Intervals shorter than one minute are not supported by Sentry Crons. Monitor '%s' has an interval of %s seconds. Use the `exclude_beat_tasks` option in the celery integration to exclude it.",
"foo",
3,
)
assert monitor_config == {}
def test_get_monitor_config_minutes():
app = MagicMock()
app.timezone = "Europe/Vienna"
# schedule with the default timezone
celery_schedule = schedule(run_every=60) # seconds
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "interval",
"value": 1,
"unit": "minute",
},
"timezone": "UTC",
}
# schedule with the timezone from the app
celery_schedule = schedule(run_every=60, app=app) # seconds
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "interval",
"value": 1,
"unit": "minute",
},
"timezone": "Europe/Vienna", # the timezone from the app
}
# schedule without a timezone, the celery integration will read the config from the app
celery_schedule = schedule(run_every=60) # seconds
celery_schedule.tz = None
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "interval",
"value": 1,
"unit": "minute",
},
"timezone": "Europe/Vienna", # the timezone from the app
}
# schedule without a timezone, and an app without timezone, the celery integration will fall back to UTC
app = MagicMock()
app.timezone = None
celery_schedule = schedule(run_every=60) # seconds
celery_schedule.tz = None
monitor_config = _get_monitor_config(celery_schedule, app, "foo")
assert monitor_config == {
"schedule": {
"type": "interval",
"value": 1,
"unit": "minute",
},
"timezone": "UTC", # default timezone from celery integration
}
def test_get_monitor_config_unknown():
app = MagicMock()
app.timezone = "Europe/Vienna"
unknown_celery_schedule = MagicMock()
monitor_config = _get_monitor_config(unknown_celery_schedule, app, "foo")
assert monitor_config == {}
def test_get_monitor_config_default_timezone():
app = MagicMock()
app.timezone = None
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
monitor_config = _get_monitor_config(celery_schedule, app, "dummy_monitor_name")
assert monitor_config["timezone"] == "UTC"
def test_get_monitor_config_timezone_in_app_conf():
app = MagicMock()
app.timezone = "Asia/Karachi"
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
celery_schedule.tz = None
monitor_config = _get_monitor_config(celery_schedule, app, "dummy_monitor_name")
assert monitor_config["timezone"] == "Asia/Karachi"
def test_get_monitor_config_timezone_in_celery_schedule():
app = MagicMock()
app.timezone = "Asia/Karachi"
panama_tz = datetime.timezone(datetime.timedelta(hours=-5), name="America/Panama")
celery_schedule = crontab(day_of_month="3", hour="12", minute="*/10")
celery_schedule.tz = panama_tz
monitor_config = _get_monitor_config(celery_schedule, app, "dummy_monitor_name")
assert monitor_config["timezone"] == str(panama_tz)
@pytest.mark.parametrize(
"task_name,exclude_beat_tasks,task_in_excluded_beat_tasks",
[
["some_task_name", ["xxx", "some_task.*"], True],
["some_task_name", ["xxx", "some_other_task.*"], False],
],
)
def test_exclude_beat_tasks_option(
task_name, exclude_beat_tasks, task_in_excluded_beat_tasks
):
"""
Test excluding Celery Beat tasks from automatic instrumentation.
"""
fake_apply_entry = MagicMock()
fake_scheduler = MagicMock()
fake_scheduler.apply_entry = fake_apply_entry
fake_integration = MagicMock()
fake_integration.exclude_beat_tasks = exclude_beat_tasks
fake_client = MagicMock()
fake_client.get_integration.return_value = fake_integration
fake_schedule_entry = MagicMock()
fake_schedule_entry.name = task_name
fake_get_monitor_config = MagicMock()
with mock.patch(
"sentry_sdk.integrations.celery.beat.Scheduler", fake_scheduler
) as Scheduler: # noqa: N806
with mock.patch(
"sentry_sdk.integrations.celery.sentry_sdk.get_client",
return_value=fake_client,
):
with mock.patch(
"sentry_sdk.integrations.celery.beat._get_monitor_config",
fake_get_monitor_config,
) as _get_monitor_config:
# Mimic CeleryIntegration patching of Scheduler.apply_entry()
_patch_beat_apply_entry()
# Mimic Celery Beat calling a task from the Beat schedule
Scheduler.apply_entry(fake_scheduler, fake_schedule_entry)
if task_in_excluded_beat_tasks:
# Only the original Scheduler.apply_entry() is called, _get_monitor_config is NOT called.
assert fake_apply_entry.call_count == 1
_get_monitor_config.assert_not_called()
else:
# The original Scheduler.apply_entry() is called, AND _get_monitor_config is called.
assert fake_apply_entry.call_count == 1
assert _get_monitor_config.call_count == 1
@pytest.mark.parametrize(
"task_name,exclude_beat_tasks,task_in_excluded_beat_tasks",
[
["some_task_name", ["xxx", "some_task.*"], True],
["some_task_name", ["xxx", "some_other_task.*"], False],
],
)
def test_exclude_redbeat_tasks_option(
task_name, exclude_beat_tasks, task_in_excluded_beat_tasks
):
"""
Test excluding Celery RedBeat tasks from automatic instrumentation.
"""
fake_apply_async = MagicMock()
fake_redbeat_scheduler = MagicMock()
fake_redbeat_scheduler.apply_async = fake_apply_async
fake_integration = MagicMock()
fake_integration.exclude_beat_tasks = exclude_beat_tasks
fake_client = MagicMock()
fake_client.get_integration.return_value = fake_integration
fake_schedule_entry = MagicMock()
fake_schedule_entry.name = task_name
fake_get_monitor_config = MagicMock()
with mock.patch(
"sentry_sdk.integrations.celery.beat.RedBeatScheduler", fake_redbeat_scheduler
) as RedBeatScheduler: # noqa: N806
with mock.patch(
"sentry_sdk.integrations.celery.sentry_sdk.get_client",
return_value=fake_client,
):
with mock.patch(
"sentry_sdk.integrations.celery.beat._get_monitor_config",
fake_get_monitor_config,
) as _get_monitor_config:
# Mimic CeleryIntegration patching of RedBeatScheduler.apply_async()
_patch_redbeat_apply_async()
# Mimic Celery RedBeat calling a task from the RedBeat schedule
RedBeatScheduler.apply_async(
fake_redbeat_scheduler, fake_schedule_entry
)
if task_in_excluded_beat_tasks:
# Only the original RedBeatScheduler.maybe_due() is called, _get_monitor_config is NOT called.
assert fake_apply_async.call_count == 1
_get_monitor_config.assert_not_called()
else:
# The original RedBeatScheduler.maybe_due() is called, AND _get_monitor_config is called.
assert fake_apply_async.call_count == 1
assert _get_monitor_config.call_count == 1
sentry-python-2.64.0/tests/integrations/celery/test_update_celery_task_headers.py 0000664 0000000 0000000 00000021454 15220673775 0030600 0 ustar 00root root 0000000 0000000 import itertools
from copy import copy
from unittest import mock
import pytest
import sentry_sdk
from sentry_sdk.integrations.celery import _update_celery_task_headers
from sentry_sdk.tracing_utils import Baggage
BAGGAGE_VALUE = (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=0.1337,"
"custom=value"
)
SENTRY_TRACE_VALUE = "771a43a4192642f0b136d5159a501700-1234567890abcdef-1"
@pytest.mark.parametrize("monitor_beat_tasks", [True, False, None, "", "bla", 1, 0])
def test_monitor_beat_tasks(monitor_beat_tasks):
headers = {}
span = None
outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks)
assert headers == {} # left unchanged
if monitor_beat_tasks:
assert outgoing_headers["sentry-monitor-start-timestamp-s"] == mock.ANY
assert (
outgoing_headers["headers"]["sentry-monitor-start-timestamp-s"] == mock.ANY
)
else:
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"]
@pytest.mark.parametrize("monitor_beat_tasks", [True, False, None, "", "bla", 1, 0])
def test_monitor_beat_tasks_with_headers(monitor_beat_tasks):
headers = {
"blub": "foo",
"sentry-something": "bar",
"sentry-task-enqueued-time": mock.ANY,
}
span = None
outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks)
assert headers == {
"blub": "foo",
"sentry-something": "bar",
"sentry-task-enqueued-time": mock.ANY,
} # left unchanged
if monitor_beat_tasks:
assert outgoing_headers["blub"] == "foo"
assert outgoing_headers["sentry-something"] == "bar"
assert outgoing_headers["sentry-monitor-start-timestamp-s"] == mock.ANY
assert outgoing_headers["headers"]["sentry-something"] == "bar"
assert (
outgoing_headers["headers"]["sentry-monitor-start-timestamp-s"] == mock.ANY
)
else:
assert outgoing_headers["blub"] == "foo"
assert outgoing_headers["sentry-something"] == "bar"
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"]
def test_span_with_transaction(sentry_init):
sentry_init(traces_sample_rate=1.0)
headers = {}
monitor_beat_tasks = False
with sentry_sdk.start_transaction(name="test_transaction") as transaction:
with sentry_sdk.start_span(op="test_span") as span:
outgoing_headers = _update_celery_task_headers(
headers, span, monitor_beat_tasks
)
assert outgoing_headers["sentry-trace"] == span.to_traceparent()
assert outgoing_headers["headers"]["sentry-trace"] == span.to_traceparent()
assert outgoing_headers["baggage"] == transaction.get_baggage().serialize()
assert (
outgoing_headers["headers"]["baggage"]
== transaction.get_baggage().serialize()
)
def test_span_with_transaction_custom_headers(sentry_init):
sentry_init(traces_sample_rate=1.0)
headers = {
"baggage": BAGGAGE_VALUE,
"sentry-trace": SENTRY_TRACE_VALUE,
}
with sentry_sdk.start_transaction(name="test_transaction") as transaction:
with sentry_sdk.start_span(op="test_span") as span:
outgoing_headers = _update_celery_task_headers(headers, span, False)
assert outgoing_headers["sentry-trace"] == span.to_traceparent()
assert outgoing_headers["headers"]["sentry-trace"] == span.to_traceparent()
incoming_baggage = Baggage.from_incoming_header(headers["baggage"])
combined_baggage = copy(transaction.get_baggage())
combined_baggage.sentry_items.update(incoming_baggage.sentry_items)
combined_baggage.third_party_items = ",".join(
[
x
for x in [
combined_baggage.third_party_items,
incoming_baggage.third_party_items,
]
if x is not None and x != ""
]
)
assert outgoing_headers["baggage"] == combined_baggage.serialize(
include_third_party=True
)
assert outgoing_headers["headers"]["baggage"] == combined_baggage.serialize(
include_third_party=True
)
@pytest.mark.parametrize("monitor_beat_tasks", [True, False])
def test_celery_trace_propagation_default(sentry_init, monitor_beat_tasks):
"""
The celery integration does not check the traces_sample_rate.
By default traces_sample_rate is None which means "do not propagate traces".
But the celery integration does not check this value.
The Celery integration has its own mechanism to propagate traces:
https://docs.sentry.io/platforms/python/integrations/celery/#distributed-traces
"""
sentry_init()
headers = {}
span = None
scope = sentry_sdk.get_isolation_scope()
outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks)
assert outgoing_headers["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["headers"]["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["baggage"] == scope.get_baggage().serialize()
assert outgoing_headers["headers"]["baggage"] == scope.get_baggage().serialize()
if monitor_beat_tasks:
assert "sentry-monitor-start-timestamp-s" in outgoing_headers
assert "sentry-monitor-start-timestamp-s" in outgoing_headers["headers"]
else:
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"]
@pytest.mark.parametrize(
"traces_sample_rate,monitor_beat_tasks",
list(itertools.product([None, 0, 0.0, 0.5, 1.0, 1, 2], [True, False])),
)
def test_celery_trace_propagation_traces_sample_rate(
sentry_init, traces_sample_rate, monitor_beat_tasks
):
"""
The celery integration does not check the traces_sample_rate.
By default traces_sample_rate is None which means "do not propagate traces".
But the celery integration does not check this value.
The Celery integration has its own mechanism to propagate traces:
https://docs.sentry.io/platforms/python/integrations/celery/#distributed-traces
"""
sentry_init(traces_sample_rate=traces_sample_rate)
headers = {}
span = None
scope = sentry_sdk.get_isolation_scope()
outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks)
assert outgoing_headers["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["headers"]["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["baggage"] == scope.get_baggage().serialize()
assert outgoing_headers["headers"]["baggage"] == scope.get_baggage().serialize()
if monitor_beat_tasks:
assert "sentry-monitor-start-timestamp-s" in outgoing_headers
assert "sentry-monitor-start-timestamp-s" in outgoing_headers["headers"]
else:
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"]
@pytest.mark.parametrize(
"enable_tracing,monitor_beat_tasks",
list(itertools.product([None, True, False], [True, False])),
)
def test_celery_trace_propagation_enable_tracing(
sentry_init, enable_tracing, monitor_beat_tasks
):
"""
The celery integration does not check the traces_sample_rate.
By default traces_sample_rate is None which means "do not propagate traces".
But the celery integration does not check this value.
The Celery integration has its own mechanism to propagate traces:
https://docs.sentry.io/platforms/python/integrations/celery/#distributed-traces
"""
sentry_init(enable_tracing=enable_tracing)
headers = {}
span = None
scope = sentry_sdk.get_isolation_scope()
outgoing_headers = _update_celery_task_headers(headers, span, monitor_beat_tasks)
assert outgoing_headers["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["headers"]["sentry-trace"] == scope.get_traceparent()
assert outgoing_headers["baggage"] == scope.get_baggage().serialize()
assert outgoing_headers["headers"]["baggage"] == scope.get_baggage().serialize()
if monitor_beat_tasks:
assert "sentry-monitor-start-timestamp-s" in outgoing_headers
assert "sentry-monitor-start-timestamp-s" in outgoing_headers["headers"]
else:
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers
assert "sentry-monitor-start-timestamp-s" not in outgoing_headers["headers"]
sentry-python-2.64.0/tests/integrations/chalice/ 0000775 0000000 0000000 00000000000 15220673775 0021724 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/chalice/__init__.py 0000664 0000000 0000000 00000000056 15220673775 0024036 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("chalice")
sentry-python-2.64.0/tests/integrations/chalice/test_chalice.py 0000664 0000000 0000000 00000016236 15220673775 0024735 0 ustar 00root root 0000000 0000000 import time
import pytest
from chalice import BadRequestError, Chalice
from chalice.local import LambdaContext, LocalGateway
from pytest_chalice.handlers import RequestHandler
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations.chalice import CHALICE_VERSION, ChaliceIntegration
from sentry_sdk.utils import parse_version
def _populate_lambda_context(context):
fn = context.function_name
context.invoked_function_arn = (
f"arn:aws:lambda:us-east-1:123456789012:function:{fn}"
)
context.log_group_name = f"/aws/lambda/{fn}"
context.log_stream_name = "2024/01/01/[$LATEST]abcdef1234567890"
context.aws_request_id = "test-request-id-1234"
return context
def _generate_lambda_context(self):
# Monkeypatch of the function _generate_lambda_context
# from the class LocalGateway
# for mock the timeout
# type: () -> LambdaContext
if self._config.lambda_timeout is None:
timeout = 10 * 1000
else:
timeout = self._config.lambda_timeout * 1000
context = LambdaContext(
function_name=self._config.function_name,
memory_size=self._config.lambda_memory_size,
max_runtime_ms=timeout,
)
return _populate_lambda_context(context)
@pytest.fixture
def app(sentry_init):
sentry_init(integrations=[ChaliceIntegration()])
app = Chalice(app_name="sentry_chalice")
@app.route("/boom")
def boom():
raise Exception("boom goes the dynamite!")
@app.route("/context")
def has_request():
raise Exception("boom goes the dynamite!")
@app.route("/badrequest")
def badrequest():
raise BadRequestError("bad-request")
@app.route("/message")
def hi():
capture_message("hi")
return {"status": "ok"}
@app.route("/message/{message_id}")
def hi_with_id(message_id):
capture_message("hi again")
return {"status": "ok"}
LocalGateway._generate_lambda_context = _generate_lambda_context
return app
@pytest.fixture
def lambda_context_args():
return ["lambda_name", 256]
def test_exception_boom(app, client: RequestHandler) -> None:
response = client.get("/boom")
assert response.status_code == 500
assert response.json == {
"Code": "InternalServerError",
"Message": "An internal server error occurred.",
}
def test_has_request(app, capture_events, client: RequestHandler):
events = capture_events()
response = client.get("/context")
assert response.status_code == 500
(event,) = events
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "Exception"
def test_scheduled_event(app, lambda_context_args):
@app.schedule("rate(1 minutes)")
def every_hour(event):
raise Exception("schedule event!")
context = _populate_lambda_context(
LambdaContext(*lambda_context_args, max_runtime_ms=10000, time_source=time)
)
lambda_event = {
"version": "0",
"account": "120987654312",
"region": "us-west-1",
"detail": {},
"detail-type": "Scheduled Event",
"source": "aws.events",
"time": "1970-01-01T00:00:00Z",
"id": "event-id",
"resources": ["arn:aws:events:us-west-1:120987654312:rule/my-schedule"],
}
with pytest.raises(Exception) as exc_info:
every_hour(lambda_event, context=context)
assert str(exc_info.value) == "schedule event!"
@pytest.mark.skipif(
parse_version(CHALICE_VERSION) >= (1, 26, 0),
reason="different behavior based on chalice version",
)
def test_bad_request_old(client: RequestHandler) -> None:
response = client.get("/badrequest")
assert response.status_code == 400
assert response.json == {
"Code": "BadRequestError",
"Message": "BadRequestError: bad-request",
}
@pytest.mark.skipif(
parse_version(CHALICE_VERSION) < (1, 26, 0),
reason="different behavior based on chalice version",
)
def test_bad_request(client: RequestHandler) -> None:
response = client.get("/badrequest")
assert response.status_code == 400
assert response.json == {
"Code": "BadRequestError",
"Message": "bad-request",
}
@pytest.mark.parametrize(
"url,expected_transaction,expected_source",
[
("/message", "api_handler", "component"),
("/message/123456", "api_handler", "component"),
],
)
def test_transaction(
app,
client: RequestHandler,
capture_events,
url,
expected_transaction,
expected_source,
):
events = capture_events()
response = client.get(url)
assert response.status_code == 200
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
def _make_span_streaming_app(sentry_init):
sentry_init(
integrations=[ChaliceIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
app = Chalice(app_name="sentry_chalice")
@app.route("/message")
def hi():
capture_message("hi")
return {"status": "ok"}
@app.route("/boom")
def boom():
raise Exception("boom goes the dynamite!")
LocalGateway._generate_lambda_context = _generate_lambda_context
return app
def test_span_streaming_existing_span(
sentry_init,
capture_items,
):
"""When a segment already exists (e.g. created by the AWS Lambda
integration), Chalice decorates it instead of creating a duplicate."""
app = _make_span_streaming_app(sentry_init)
client = RequestHandler(app)
items = capture_items("span")
with sentry_sdk.traces.start_span(
name="lambda_segment",
parent_span=None,
attributes={
"sentry.origin": "auto.function.aws_lambda",
"sentry.op": "function.aws",
"faas.name": "api_handler",
},
):
response = client.get("/message")
assert response.status_code == 200
sentry_sdk.flush()
segment_spans = [s.payload for s in items if s.payload.get("is_segment")]
assert len(segment_spans) == 1
span = segment_spans[0]
attrs = span["attributes"]
assert attrs["sentry.origin"] == "auto.function.chalice"
assert attrs["sentry.op"] == "function.aws"
assert attrs["faas.name"] == "api_handler"
assert span["status"] == "ok"
def test_span_streaming_existing_span_error(
sentry_init,
capture_items,
):
app = _make_span_streaming_app(sentry_init)
client = RequestHandler(app)
items = capture_items("event", "span")
with sentry_sdk.traces.start_span(
name="lambda_segment",
parent_span=None,
attributes={"sentry.origin": "auto.function.aws_lambda"},
):
response = client.get("/boom")
assert response.status_code == 500
sentry_sdk.flush()
error_items = [i for i in items if i.type == "event"]
assert len(error_items) == 1
segment_spans = [
s.payload for s in items if s.type == "span" and s.payload.get("is_segment")
]
assert len(segment_spans) == 1
assert segment_spans[0]["attributes"]["sentry.origin"] == "auto.function.chalice"
assert segment_spans[0]["status"] == "error"
sentry-python-2.64.0/tests/integrations/clickhouse_driver/ 0000775 0000000 0000000 00000000000 15220673775 0024040 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/clickhouse_driver/__init__.py 0000664 0000000 0000000 00000000070 15220673775 0026146 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("clickhouse_driver")
sentry-python-2.64.0/tests/integrations/clickhouse_driver/test_clickhouse_driver.py 0000664 0000000 0000000 00000152254 15220673775 0031166 0 ustar 00root root 0000000 0000000 """
Tests need a local clickhouse instance running, this can best be done using
```sh
docker run -d -p 18123:8123 -p9000:9000 --name clickhouse-test --ulimit nofile=262144:262144 --rm clickhouse/clickhouse-server
```
"""
from unittest import mock
import clickhouse_driver
import pytest
from clickhouse_driver import Client, connect
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.integrations.clickhouse_driver import ClickhouseDriverIntegration
from tests.conftest import ApproxDict
EXPECT_PARAMS_IN_SELECT = True
if clickhouse_driver.VERSION < (0, 2, 6):
EXPECT_PARAMS_IN_SELECT = False
def test_clickhouse_client_breadcrumbs(sentry_init, capture_events) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={"record_sql_params": True},
)
events = capture_events()
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
assert res[0][0] == 370
capture_message("hi")
(event,) = events
expected_breadcrumbs = [
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "DROP TABLE IF EXISTS test",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "SELECT sum(x) FROM test WHERE x > 150",
"type": "default",
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_breadcrumbs[-1]["data"].pop("db.params", None)
for crumb in expected_breadcrumbs:
crumb["data"] = ApproxDict(crumb["data"])
for crumb in event["breadcrumbs"]["values"]:
crumb.pop("timestamp", None)
actual_query_breadcrumbs = [
breadcrumb
for breadcrumb in event["breadcrumbs"]["values"]
if breadcrumb["category"] == "query"
]
assert actual_query_breadcrumbs == expected_breadcrumbs
def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
send_default_pii=True,
_experiments={"record_sql_params": True},
)
events = capture_events()
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
assert res[0][0] == 370
capture_message("hi")
(event,) = events
expected_breadcrumbs = [
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [],
},
"message": "DROP TABLE IF EXISTS test",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [],
},
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [{"x": 100}],
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [[170], [200]],
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [[370]],
"db.params": {"minv": 150},
},
"message": "SELECT sum(x) FROM test WHERE x > 150",
"type": "default",
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_breadcrumbs[-1]["data"].pop("db.params", None)
for crumb in expected_breadcrumbs:
crumb["data"] = ApproxDict(crumb["data"])
for crumb in event["breadcrumbs"]["values"]:
crumb.pop("timestamp", None)
assert event["breadcrumbs"]["values"] == expected_breadcrumbs
@pytest.mark.parametrize("span_streaming", [True, False])
def test_clickhouse_client_spans(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
traces_sample_rate=1.0,
)
if span_streaming:
items = capture_items("span")
trace_id = None
span_id = None
with sentry_sdk.traces.start_span(name="custom parent") as span:
trace_id = span.trace_id
span_id = span.span_id
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute(
"SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150}
)
assert res[0][0] == 370
sentry_sdk.flush()
spans = [item.payload for item in items]
expected_spans = [
{
"name": "DROP TABLE IF EXISTS test",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"sentry.op": "db",
"db.query.text": "DROP TABLE IF EXISTS test",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "CREATE TABLE test (x Int32) ENGINE = Memory",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"sentry.op": "db",
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"sentry.op": "db",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"sentry.op": "db",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "SELECT sum(x) FROM test WHERE x > 150",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"sentry.op": "db",
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "custom parent",
"attributes": [],
"trace_id": trace_id,
},
]
for span in expected_spans:
span["attributes"] = ApproxDict(span["attributes"])
for span in spans:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("end_timestamp", None)
span.pop("is_segment", None)
span.pop("status", None)
assert spans == expected_spans
else:
events = capture_events()
transaction_trace_id = None
transaction_span_id = None
with start_transaction(name="test_clickhouse_transaction") as transaction:
transaction_trace_id = transaction.trace_id
transaction_span_id = transaction.span_id
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute(
"SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150}
)
assert res[0][0] == 370
(event,) = events
expected_spans = [
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "DROP TABLE IF EXISTS test",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "CREATE TABLE test (x Int32) ENGINE = Memory",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "SELECT sum(x) FROM test WHERE x > 150",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_spans[-1]["data"].pop("db.params", None)
for span in expected_spans:
span["data"] = ApproxDict(span["data"])
for span in event["spans"]:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("timestamp", None)
assert event["spans"] == expected_spans
def test_clickhouse_spans_with_generator(sentry_init, capture_events):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
)
events = capture_events()
# Use a generator to test that the integration obtains values from the generator,
# without consuming the generator.
values = ({"x": i} for i in range(3))
with start_transaction(name="test_clickhouse_transaction"):
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", values)
res = client.execute("SELECT x FROM test")
# Verify that the integration did not consume the generator
assert res == [(0,), (1,), (2,)]
(event,) = events
spans = event["spans"]
[span] = [
span for span in spans if span["description"] == "INSERT INTO test (x) VALUES"
]
assert span["data"]["db.params"] == [{"x": 0}, {"x": 1}, {"x": 2}]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_clickhouse_client_spans_with_pii(
sentry_init,
capture_events,
capture_items,
capture_envelopes,
span_streaming,
):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
traces_sample_rate=1.0,
send_default_pii=True,
)
if span_streaming:
items = capture_items("span")
trace_id = None
span_id = None
with sentry_sdk.traces.start_span(name="custom parent") as span:
trace_id = span.trace_id
span_id = span.span_id
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute(
"SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150}
)
assert res[0][0] == 370
sentry_sdk.flush()
spans = [item.payload for item in items]
expected_spans = [
{
"name": "DROP TABLE IF EXISTS test",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "DROP TABLE IF EXISTS test",
"server.address": "localhost",
"server.port": 9000,
"thread.id": mock.ANY,
"thread.name": mock.ANY,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "CREATE TABLE test (x Int32) ENGINE = Memory",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
"server.address": "localhost",
"server.port": 9000,
"thread.id": mock.ANY,
"thread.name": mock.ANY,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"server.address": "localhost",
"server.port": 9000,
"thread.id": mock.ANY,
"thread.name": mock.ANY,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "SELECT sum(x) FROM test WHERE x > 150",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "custom parent",
"attributes": [],
"trace_id": trace_id,
},
]
for span in expected_spans:
span["attributes"] = ApproxDict(span["attributes"])
for span in spans:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("end_timestamp", None)
span.pop("is_segment", None)
span.pop("status", None)
assert spans == expected_spans
else:
events = capture_events()
transaction_trace_id = None
transaction_span_id = None
with start_transaction(name="test_clickhouse_transaction") as transaction:
transaction_trace_id = transaction.trace_id
transaction_span_id = transaction.span_id
client = Client("localhost")
client.execute("DROP TABLE IF EXISTS test")
client.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
client.execute("INSERT INTO test (x) VALUES", [{"x": 100}])
client.execute("INSERT INTO test (x) VALUES", [[170], [200]])
res = client.execute(
"SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150}
)
assert res[0][0] == 370
(event,) = events
expected_spans = [
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "DROP TABLE IF EXISTS test",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "CREATE TABLE test (x Int32) ENGINE = Memory",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [{"x": 100}],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [[170], [200]],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "SELECT sum(x) FROM test WHERE x > 150",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": {"minv": 150},
"db.result": [[370]],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_spans[-1]["data"].pop("db.params", None)
for span in expected_spans:
span["data"] = ApproxDict(span["data"])
for span in event["spans"]:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("timestamp", None)
assert event["spans"] == expected_spans
def test_clickhouse_dbapi_breadcrumbs(sentry_init, capture_events) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
)
events = capture_events()
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
assert res[0][0] == 370
capture_message("hi")
(event,) = events
expected_breadcrumbs = [
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "DROP TABLE IF EXISTS test",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"message": "SELECT sum(x) FROM test WHERE x > 150",
"type": "default",
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_breadcrumbs[-1]["data"].pop("db.params", None)
for crumb in expected_breadcrumbs:
crumb["data"] = ApproxDict(crumb["data"])
for crumb in event["breadcrumbs"]["values"]:
crumb.pop("timestamp", None)
assert event["breadcrumbs"]["values"] == expected_breadcrumbs
def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
send_default_pii=True,
)
events = capture_events()
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
assert res[0][0] == 370
capture_message("hi")
(event,) = events
expected_breadcrumbs = [
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [[], []],
},
"message": "DROP TABLE IF EXISTS test",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [[], []],
},
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [{"x": 100}],
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [[170], [200]],
},
"message": "INSERT INTO test (x) VALUES",
"type": "default",
},
{
"category": "query",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": {"minv": 150},
"db.result": [[["370"]], [["'sum(x)'", "'Int64'"]]],
},
"message": "SELECT sum(x) FROM test WHERE x > 150",
"type": "default",
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_breadcrumbs[-1]["data"].pop("db.params", None)
for crumb in expected_breadcrumbs:
crumb["data"] = ApproxDict(crumb["data"])
for crumb in event["breadcrumbs"]["values"]:
crumb.pop("timestamp", None)
assert event["breadcrumbs"]["values"] == expected_breadcrumbs
@pytest.mark.parametrize("span_streaming", [True, False])
def test_clickhouse_dbapi_spans(
sentry_init,
capture_events,
capture_items,
capture_envelopes,
span_streaming,
):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
traces_sample_rate=1.0,
)
if span_streaming:
items = capture_items("span")
trace_id = None
span_id = None
with sentry_sdk.traces.start_span(name="custom parent") as span:
trace_id = span.trace_id
span_id = span.span_id
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
sentry_sdk.flush()
spans = [item.payload for item in items]
expected_spans = [
{
"name": "DROP TABLE IF EXISTS test",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"db.query.text": "DROP TABLE IF EXISTS test",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "CREATE TABLE test (x Int32) ENGINE = Memory",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "SELECT sum(x) FROM test WHERE x > 150",
"attributes": {
"db.system.name": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.namespace": "",
"db.user": "default",
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "custom parent",
"attributes": [],
"trace_id": trace_id,
},
]
for span in expected_spans:
span["attributes"] = ApproxDict(span["attributes"])
for span in spans:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("end_timestamp", None)
span.pop("is_segment", None)
span.pop("status", None)
assert spans == expected_spans
else:
events = capture_events()
transaction_trace_id = None
transaction_span_id = None
with start_transaction(name="test_clickhouse_transaction") as transaction:
transaction_trace_id = transaction.trace_id
transaction_span_id = transaction.span_id
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
assert res[0][0] == 370
(event,) = events
expected_spans = [
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "DROP TABLE IF EXISTS test",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "CREATE TABLE test (x Int32) ENGINE = Memory",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "SELECT sum(x) FROM test WHERE x > 150",
"data": {
"db.system": "clickhouse",
"db.driver.name": "clickhouse-driver",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_spans[-1]["data"].pop("db.params", None)
for span in expected_spans:
span["data"] = ApproxDict(span["data"])
for span in event["spans"]:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("timestamp", None)
assert event["spans"] == expected_spans
@pytest.mark.parametrize("span_streaming", [True, False])
def test_clickhouse_dbapi_spans_with_pii(
sentry_init,
capture_events,
capture_items,
capture_envelopes,
span_streaming,
):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
traces_sample_rate=1.0,
send_default_pii=True,
)
if span_streaming:
items = capture_items("span")
trace_id = None
span_id = None
with sentry_sdk.traces.start_span(name="custom parent") as span:
trace_id = span.trace_id
span_id = span.span_id
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
assert res[0][0] == 370
sentry_sdk.flush()
spans = [item.payload for item in items]
expected_spans = [
{
"name": "DROP TABLE IF EXISTS test",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "DROP TABLE IF EXISTS test",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "CREATE TABLE test (x Int32) ENGINE = Memory",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "INSERT INTO test (x) VALUES",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "INSERT INTO test (x) VALUES",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "SELECT sum(x) FROM test WHERE x > 150",
"attributes": {
"db.system.name": "clickhouse",
"db.namespace": "",
"db.user": "default",
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
"sentry.op": "db",
"sentry.origin": "auto.db.clickhouse_driver",
"server.address": "localhost",
"server.port": 9000,
},
"trace_id": trace_id,
"parent_span_id": span_id,
},
{
"name": "custom parent",
"attributes": [],
"trace_id": trace_id,
},
]
for span in expected_spans:
span["attributes"] = ApproxDict(span["attributes"])
for span in spans:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("end_timestamp", None)
span.pop("is_segment", None)
span.pop("status", None)
assert spans == expected_spans
else:
events = capture_events()
transaction_trace_id = None
transaction_span_id = None
with start_transaction(name="test_clickhouse_transaction") as transaction:
transaction_trace_id = transaction.trace_id
transaction_span_id = transaction.span_id
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS test")
cursor.execute("CREATE TABLE test (x Int32) ENGINE = Memory")
cursor.executemany("INSERT INTO test (x) VALUES", [{"x": 100}])
cursor.executemany("INSERT INTO test (x) VALUES", [[170], [200]])
cursor.execute("SELECT sum(x) FROM test WHERE x > %(minv)i", {"minv": 150})
res = cursor.fetchall()
assert res[0][0] == 370
(event,) = events
expected_spans = [
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "DROP TABLE IF EXISTS test",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [[], []],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "CREATE TABLE test (x Int32) ENGINE = Memory",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.result": [[], []],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [{"x": 100}],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "INSERT INTO test (x) VALUES",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": [[170], [200]],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
{
"op": "db",
"origin": "auto.db.clickhouse_driver",
"description": "SELECT sum(x) FROM test WHERE x > 150",
"data": {
"db.system": "clickhouse",
"db.name": "",
"db.user": "default",
"server.address": "localhost",
"server.port": 9000,
"db.params": {"minv": 150},
"db.result": [[[370]], [["sum(x)", "Int64"]]],
},
"same_process_as_parent": True,
"trace_id": transaction_trace_id,
"parent_span_id": transaction_span_id,
},
]
if not EXPECT_PARAMS_IN_SELECT:
expected_spans[-1]["data"].pop("db.params", None)
for span in expected_spans:
span["data"] = ApproxDict(span["data"])
for span in event["spans"]:
span.pop("span_id", None)
span.pop("start_timestamp", None)
span.pop("timestamp", None)
assert event["spans"] == expected_spans
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(
sentry_init,
capture_events,
capture_items,
capture_envelopes,
span_streaming,
):
sentry_init(
integrations=[ClickhouseDriverIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("SELECT 1")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[1]["attributes"]["sentry.origin"] == "manual"
assert spans[0]["attributes"]["sentry.origin"] == "auto.db.clickhouse_driver"
else:
events = capture_events()
with start_transaction(name="test_clickhouse_transaction"):
conn = connect("clickhouse://localhost")
cursor = conn.cursor()
cursor.execute("SELECT 1")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.clickhouse_driver"
sentry-python-2.64.0/tests/integrations/cloud_resource_context/ 0000775 0000000 0000000 00000000000 15220673775 0025115 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/cloud_resource_context/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0027214 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/cloud_resource_context/test_cloud_resource_context.py 0000664 0000000 0000000 00000031526 15220673775 0033316 0 ustar 00root root 0000000 0000000 import json
from unittest import mock
from unittest.mock import MagicMock
import pytest
from sentry_sdk.integrations.cloud_resource_context import (
CLOUD_PLATFORM,
CLOUD_PROVIDER,
)
AWS_EC2_EXAMPLE_IMDSv2_PAYLOAD = {
"accountId": "298817902971",
"architecture": "x86_64",
"availabilityZone": "us-east-1b",
"billingProducts": None,
"devpayProductCodes": None,
"marketplaceProductCodes": None,
"imageId": "ami-00874d747dde344fa",
"instanceId": "i-07d3301297fe0a55a",
"instanceType": "t2.small",
"kernelId": None,
"pendingTime": "2023-02-08T07:54:05Z",
"privateIp": "171.131.65.115",
"ramdiskId": None,
"region": "us-east-1",
"version": "2017-09-30",
}
AWS_EC2_EXAMPLE_IMDSv2_PAYLOAD_BYTES = bytes(
json.dumps(AWS_EC2_EXAMPLE_IMDSv2_PAYLOAD), "utf-8"
)
GCP_GCE_EXAMPLE_METADATA_PLAYLOAD = {
"instance": {
"attributes": {},
"cpuPlatform": "Intel Broadwell",
"description": "",
"disks": [
{
"deviceName": "tests-cloud-contexts-in-python-sdk",
"index": 0,
"interface": "SCSI",
"mode": "READ_WRITE",
"type": "PERSISTENT-BALANCED",
}
],
"guestAttributes": {},
"hostname": "tests-cloud-contexts-in-python-sdk.c.client-infra-internal.internal",
"id": 1535324527892303790,
"image": "projects/debian-cloud/global/images/debian-11-bullseye-v20221206",
"licenses": [{"id": "2853224013536823851"}],
"machineType": "projects/542054129475/machineTypes/e2-medium",
"maintenanceEvent": "NONE",
"name": "tests-cloud-contexts-in-python-sdk",
"networkInterfaces": [
{
"accessConfigs": [
{"externalIp": "134.30.53.15", "type": "ONE_TO_ONE_NAT"}
],
"dnsServers": ["169.254.169.254"],
"forwardedIps": [],
"gateway": "10.188.0.1",
"ip": "10.188.0.3",
"ipAliases": [],
"mac": "42:01:0c:7c:00:13",
"mtu": 1460,
"network": "projects/544954029479/networks/default",
"subnetmask": "255.255.240.0",
"targetInstanceIps": [],
}
],
"preempted": "FALSE",
"remainingCpuTime": -1,
"scheduling": {
"automaticRestart": "TRUE",
"onHostMaintenance": "MIGRATE",
"preemptible": "FALSE",
},
"serviceAccounts": {},
"tags": ["http-server", "https-server"],
"virtualClock": {"driftToken": "0"},
"zone": "projects/142954069479/zones/northamerica-northeast2-b",
},
"oslogin": {"authenticate": {"sessions": {}}},
"project": {
"attributes": {},
"numericProjectId": 204954049439,
"projectId": "my-project-internal",
},
}
try:
# Python 3
GCP_GCE_EXAMPLE_METADATA_PLAYLOAD_BYTES = bytes(
json.dumps(GCP_GCE_EXAMPLE_METADATA_PLAYLOAD), "utf-8"
)
except TypeError:
# Python 2
GCP_GCE_EXAMPLE_METADATA_PLAYLOAD_BYTES = bytes(
json.dumps(GCP_GCE_EXAMPLE_METADATA_PLAYLOAD)
).encode("utf-8")
def test_is_aws_http_error():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
response = MagicMock()
response.status = 405
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._is_aws() is False
assert CloudResourceContextIntegration.aws_token == ""
def test_is_aws_ok():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
response = MagicMock()
response.status = 200
response.data = b"something"
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._is_aws() is True
assert CloudResourceContextIntegration.aws_token == "something"
CloudResourceContextIntegration.http.request = MagicMock(
side_effect=Exception("Test")
)
assert CloudResourceContextIntegration._is_aws() is False
def test_is_aw_exception():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(
side_effect=Exception("Test")
)
assert CloudResourceContextIntegration._is_aws() is False
@pytest.mark.parametrize(
"http_status, response_data, expected_context",
[
[
405,
b"",
{
"cloud.provider": CLOUD_PROVIDER.AWS,
"cloud.platform": CLOUD_PLATFORM.AWS_EC2,
},
],
[
200,
b"something-but-not-json",
{
"cloud.provider": CLOUD_PROVIDER.AWS,
"cloud.platform": CLOUD_PLATFORM.AWS_EC2,
},
],
[
200,
AWS_EC2_EXAMPLE_IMDSv2_PAYLOAD_BYTES,
{
"cloud.provider": "aws",
"cloud.platform": "aws_ec2",
"cloud.account.id": "298817902971",
"cloud.availability_zone": "us-east-1b",
"cloud.region": "us-east-1",
"host.id": "i-07d3301297fe0a55a",
"host.type": "t2.small",
},
],
],
)
def test_get_aws_context(http_status, response_data, expected_context):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
response = MagicMock()
response.status = http_status
response.data = response_data
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._get_aws_context() == expected_context
def test_is_gcp_http_error():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
response = MagicMock()
response.status = 405
response.data = b'{"some": "json"}'
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._is_gcp() is False
assert CloudResourceContextIntegration.gcp_metadata is None
def test_is_gcp_ok():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
response = MagicMock()
response.status = 200
response.data = b'{"some": "json"}'
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._is_gcp() is True
assert CloudResourceContextIntegration.gcp_metadata == {"some": "json"}
def test_is_gcp_exception():
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(
side_effect=Exception("Test")
)
assert CloudResourceContextIntegration._is_gcp() is False
@pytest.mark.parametrize(
"http_status, response_data, expected_context",
[
[
405,
None,
{
"cloud.provider": CLOUD_PROVIDER.GCP,
"cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE,
},
],
[
200,
b"something-but-not-json",
{
"cloud.provider": CLOUD_PROVIDER.GCP,
"cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE,
},
],
[
200,
GCP_GCE_EXAMPLE_METADATA_PLAYLOAD_BYTES,
{
"cloud.provider": "gcp",
"cloud.platform": "gcp_compute_engine",
"cloud.account.id": "my-project-internal",
"cloud.availability_zone": "northamerica-northeast2-b",
"host.id": 1535324527892303790,
},
],
],
)
def test_get_gcp_context(http_status, response_data, expected_context):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration.gcp_metadata = None
response = MagicMock()
response.status = http_status
response.data = response_data
CloudResourceContextIntegration.http = MagicMock()
CloudResourceContextIntegration.http.request = MagicMock(return_value=response)
assert CloudResourceContextIntegration._get_gcp_context() == expected_context
@pytest.mark.parametrize(
"is_aws, is_gcp, expected_provider",
[
[False, False, ""],
[False, True, CLOUD_PROVIDER.GCP],
[True, False, CLOUD_PROVIDER.AWS],
[True, True, CLOUD_PROVIDER.AWS],
],
)
def test_get_cloud_provider(is_aws, is_gcp, expected_provider):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration._is_aws = MagicMock(return_value=is_aws)
CloudResourceContextIntegration._is_gcp = MagicMock(return_value=is_gcp)
assert CloudResourceContextIntegration._get_cloud_provider() == expected_provider
@pytest.mark.parametrize(
"cloud_provider",
[
CLOUD_PROVIDER.ALIBABA,
CLOUD_PROVIDER.AZURE,
CLOUD_PROVIDER.IBM,
CLOUD_PROVIDER.TENCENT,
],
)
def test_get_cloud_resource_context_unsupported_providers(cloud_provider):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration._get_cloud_provider = MagicMock(
return_value=cloud_provider
)
assert CloudResourceContextIntegration._get_cloud_resource_context() == {}
@pytest.mark.parametrize(
"cloud_provider",
[
CLOUD_PROVIDER.AWS,
CLOUD_PROVIDER.GCP,
],
)
def test_get_cloud_resource_context_supported_providers(cloud_provider):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration._get_cloud_provider = MagicMock(
return_value=cloud_provider
)
assert CloudResourceContextIntegration._get_cloud_resource_context() != {}
@pytest.mark.parametrize(
"cloud_provider, cloud_resource_context, warning_called, set_context_called",
[
["", {}, False, False],
[CLOUD_PROVIDER.AWS, {}, False, False],
[CLOUD_PROVIDER.GCP, {}, False, False],
[CLOUD_PROVIDER.AZURE, {}, True, False],
[CLOUD_PROVIDER.ALIBABA, {}, True, False],
[CLOUD_PROVIDER.IBM, {}, True, False],
[CLOUD_PROVIDER.TENCENT, {}, True, False],
["", {"some": "context"}, False, True],
[CLOUD_PROVIDER.AWS, {"some": "context"}, False, True],
[CLOUD_PROVIDER.GCP, {"some": "context"}, False, True],
],
)
def test_setup_once(
cloud_provider, cloud_resource_context, warning_called, set_context_called
):
from sentry_sdk.integrations.cloud_resource_context import (
CloudResourceContextIntegration,
)
CloudResourceContextIntegration.cloud_provider = cloud_provider
CloudResourceContextIntegration._get_cloud_resource_context = MagicMock(
return_value=cloud_resource_context
)
with mock.patch(
"sentry_sdk.integrations.cloud_resource_context.set_context"
) as fake_set_context:
with mock.patch(
"sentry_sdk.integrations.cloud_resource_context.logger.warning"
) as fake_warning:
CloudResourceContextIntegration.setup_once()
if set_context_called:
fake_set_context.assert_called_once_with(
"cloud_resource", cloud_resource_context
)
else:
fake_set_context.assert_not_called()
def invalid_value_warning_calls():
"""
Iterator that yields True if the warning was called with the expected message.
Written as a generator function, rather than a list comprehension, to allow
us to handle exceptions that might be raised during the iteration if the
warning call was not as expected.
"""
for call in fake_warning.call_args_list:
try:
yield call[0][0].startswith("Invalid value for cloud_provider:")
except (IndexError, KeyError, TypeError, AttributeError):
...
assert warning_called == any(invalid_value_warning_calls())
sentry-python-2.64.0/tests/integrations/cohere/ 0000775 0000000 0000000 00000000000 15220673775 0021601 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/cohere/__init__.py 0000664 0000000 0000000 00000000055 15220673775 0023712 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("cohere")
sentry-python-2.64.0/tests/integrations/cohere/test_cohere.py 0000664 0000000 0000000 00000035674 15220673775 0024476 0 ustar 00root root 0000000 0000000 import json
from unittest import mock # python 3.3 and above
import httpx
import pytest
from cohere import ChatMessage, Client
from httpx import Client as HTTPXClient
import sentry_sdk
from sentry_sdk import start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.cohere import CohereIntegration
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_nonstreaming_chat(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
span_streaming,
):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
return_value=httpx.Response(
200,
json={
"text": "the model response",
"meta": {
"billed_units": {
"output_tokens": 10,
"input_tokens": 20,
}
},
},
)
)
if span_streaming:
items = capture_items("span")
with start_transaction(name="cohere tx"):
response = client.chat(
model="some-model",
chat_history=[ChatMessage(role="SYSTEM", message="some context")],
message="hello",
).text
assert response == "the model response"
sentry_sdk.flush()
assert len(items) == 1
span = items[0].payload
assert span["attributes"]["sentry.op"] == "ai.chat_completions.create.cohere"
assert span["attributes"][SPANDATA.AI_MODEL_ID] == "some-model"
if send_default_pii and include_prompts:
assert (
'{"role": "system", "content": "some context"}'
in span["attributes"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
'{"role": "user", "content": "hello"}'
in span["attributes"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "the model response" in span["attributes"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["attributes"]
assert SPANDATA.AI_RESPONSES not in span["attributes"]
assert span["attributes"]["gen_ai.usage.output_tokens"] == 10
assert span["attributes"]["gen_ai.usage.input_tokens"] == 20
assert span["attributes"]["gen_ai.usage.total_tokens"] == 30
else:
events = capture_events()
with start_transaction(name="cohere tx"):
response = client.chat(
model="some-model",
chat_history=[ChatMessage(role="SYSTEM", message="some context")],
message="hello",
).text
assert response == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
assert len(tx["spans"]) == 1
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.cohere"
assert span["data"][SPANDATA.AI_MODEL_ID] == "some-model"
if send_default_pii and include_prompts:
assert (
'{"role": "system", "content": "some context"}'
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
'{"role": "user", "content": "hello"}'
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "the model response" in span["data"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["data"]["gen_ai.usage.output_tokens"] == 10
assert span["data"]["gen_ai.usage.input_tokens"] == 20
assert span["data"]["gen_ai.usage.total_tokens"] == 30
# noinspection PyTypeChecker
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_streaming_chat(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
span_streaming,
):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Client(api_key="z")
HTTPXClient.send = mock.Mock(
return_value=httpx.Response(
200,
content="\n".join(
[
json.dumps({"event_type": "text-generation", "text": "the model "}),
json.dumps({"event_type": "text-generation", "text": "response"}),
json.dumps(
{
"event_type": "stream-end",
"finish_reason": "COMPLETE",
"response": {
"text": "the model response",
"meta": {
"billed_units": {
"output_tokens": 10,
"input_tokens": 20,
}
},
},
}
),
]
),
)
)
if span_streaming:
items = capture_items("span")
with start_transaction(name="cohere tx"):
responses = list(
client.chat_stream(
model="some-model",
chat_history=[ChatMessage(role="SYSTEM", message="some context")],
message="hello",
)
)
response_string = responses[-1].response.text
assert response_string == "the model response"
sentry_sdk.flush()
assert len(items) == 1
span = items[0].payload
assert span["attributes"]["sentry.op"] == "ai.chat_completions.create.cohere"
assert span["attributes"][SPANDATA.AI_MODEL_ID] == "some-model"
if send_default_pii and include_prompts:
assert (
'{"role": "system", "content": "some context"}'
in span["attributes"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
'{"role": "user", "content": "hello"}'
in span["attributes"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "the model response" in span["attributes"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["attributes"]
assert SPANDATA.AI_RESPONSES not in span["attributes"]
assert span["attributes"]["gen_ai.usage.output_tokens"] == 10
assert span["attributes"]["gen_ai.usage.input_tokens"] == 20
assert span["attributes"]["gen_ai.usage.total_tokens"] == 30
else:
events = capture_events()
with start_transaction(name="cohere tx"):
responses = list(
client.chat_stream(
model="some-model",
chat_history=[ChatMessage(role="SYSTEM", message="some context")],
message="hello",
)
)
response_string = responses[-1].response.text
assert response_string == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
assert len(tx["spans"]) == 1
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.cohere"
assert span["data"][SPANDATA.AI_MODEL_ID] == "some-model"
if send_default_pii and include_prompts:
assert (
'{"role": "system", "content": "some context"}'
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert (
'{"role": "user", "content": "hello"}'
in span["data"][SPANDATA.AI_INPUT_MESSAGES]
)
assert "the model response" in span["data"][SPANDATA.AI_RESPONSES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["data"]["gen_ai.usage.output_tokens"] == 10
assert span["data"]["gen_ai.usage.input_tokens"] == 20
assert span["data"]["gen_ai.usage.total_tokens"] == 30
def test_bad_chat(sentry_init, capture_events):
sentry_init(integrations=[CohereIntegration()], traces_sample_rate=1.0)
events = capture_events()
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
side_effect=httpx.HTTPError("API rate limit reached")
)
with pytest.raises(httpx.HTTPError):
client.chat(model="some-model", message="hello")
(event, transaction) = events
assert event["level"] == "error"
assert transaction["contexts"]["trace"]["status"] == "internal_error"
def test_span_status_error(sentry_init, capture_events):
sentry_init(integrations=[CohereIntegration()], traces_sample_rate=1.0)
events = capture_events()
with start_transaction(name="test"):
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
side_effect=httpx.HTTPError("API rate limit reached")
)
with pytest.raises(httpx.HTTPError):
client.chat(model="some-model", message="hello")
(error, transaction) = events
assert error["level"] == "error"
assert transaction["spans"][0]["status"] == "internal_error"
assert transaction["spans"][0]["tags"]["status"] == "internal_error"
def test_span_status_error_streaming(sentry_init, capture_events, capture_items):
sentry_init(
integrations=[CohereIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
items = capture_items("span")
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
side_effect=httpx.HTTPError("API rate limit reached")
)
with start_transaction(name="test"):
with pytest.raises(httpx.HTTPError):
client.chat(model="some-model", message="hello")
sentry_sdk.flush()
assert len(items) == 1
span = items[0].payload
assert span["status"] == "error"
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_embed(
sentry_init,
capture_events,
capture_items,
send_default_pii,
include_prompts,
span_streaming,
):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
return_value=httpx.Response(
200,
json={
"response_type": "embeddings_floats",
"id": "1",
"texts": ["hello"],
"embeddings": [[1.0, 2.0, 3.0]],
"meta": {
"billed_units": {
"input_tokens": 10,
}
},
},
)
)
if span_streaming:
items = capture_items("span")
with start_transaction(name="cohere tx"):
response = client.embed(texts=["hello"], model="text-embedding-3-large")
assert len(response.embeddings[0]) == 3
sentry_sdk.flush()
assert len(items) == 1
span = items[0].payload
assert span["attributes"]["sentry.op"] == "ai.embeddings.create.cohere"
if send_default_pii and include_prompts:
assert "hello" in span["attributes"][SPANDATA.AI_INPUT_MESSAGES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["attributes"]
assert span["attributes"]["gen_ai.usage.input_tokens"] == 10
assert span["attributes"]["gen_ai.usage.total_tokens"] == 10
else:
events = capture_events()
with start_transaction(name="cohere tx"):
response = client.embed(texts=["hello"], model="text-embedding-3-large")
assert len(response.embeddings[0]) == 3
tx = events[0]
assert tx["type"] == "transaction"
assert len(tx["spans"]) == 1
span = tx["spans"][0]
assert span["op"] == "ai.embeddings.create.cohere"
if send_default_pii and include_prompts:
assert "hello" in span["data"][SPANDATA.AI_INPUT_MESSAGES]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert span["data"]["gen_ai.usage.input_tokens"] == 10
assert span["data"]["gen_ai.usage.total_tokens"] == 10
def test_span_origin_chat(sentry_init, capture_events):
sentry_init(
integrations=[CohereIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
return_value=httpx.Response(
200,
json={
"text": "the model response",
"meta": {
"billed_units": {
"output_tokens": 10,
"input_tokens": 20,
}
},
},
)
)
with start_transaction(name="cohere tx"):
client.chat(
model="some-model",
chat_history=[ChatMessage(role="SYSTEM", message="some context")],
message="hello",
).text
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.cohere"
def test_span_origin_embed(sentry_init, capture_events):
sentry_init(
integrations=[CohereIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = Client(api_key="z")
HTTPXClient.request = mock.Mock(
return_value=httpx.Response(
200,
json={
"response_type": "embeddings_floats",
"id": "1",
"texts": ["hello"],
"embeddings": [[1.0, 2.0, 3.0]],
"meta": {
"billed_units": {
"input_tokens": 10,
}
},
},
)
)
with start_transaction(name="cohere tx"):
client.embed(texts=["hello"], model="text-embedding-3-large")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.cohere"
sentry-python-2.64.0/tests/integrations/conftest.py 0000664 0000000 0000000 00000003530 15220673775 0022534 0 ustar 00root root 0000000 0000000 import pytest
import sentry_sdk
@pytest.fixture
def capture_exceptions(monkeypatch):
def inner():
errors = set()
old_capture_event_hub = sentry_sdk.Hub.capture_event
old_capture_event_scope = sentry_sdk.Scope.capture_event
def capture_event_hub(self, event, hint=None, scope=None):
"""
Can be removed when we remove push_scope and the Hub from the SDK.
"""
if hint:
if "exc_info" in hint:
error = hint["exc_info"][1]
errors.add(error)
return old_capture_event_hub(self, event, hint=hint, scope=scope)
def capture_event_scope(self, event, hint=None, scope=None):
if hint:
if "exc_info" in hint:
error = hint["exc_info"][1]
errors.add(error)
return old_capture_event_scope(self, event, hint=hint, scope=scope)
monkeypatch.setattr(sentry_sdk.Hub, "capture_event", capture_event_hub)
monkeypatch.setattr(sentry_sdk.Scope, "capture_event", capture_event_scope)
return errors
return inner
parametrize_test_configurable_status_codes = pytest.mark.parametrize(
("failed_request_status_codes", "status_code", "expected_error"),
(
(None, 500, True),
(None, 400, False),
({500, 501}, 500, True),
({500, 501}, 401, False),
({*range(400, 500)}, 401, True),
({*range(400, 500)}, 500, False),
({*range(400, 600)}, 300, False),
({*range(400, 600)}, 403, True),
({*range(400, 600)}, 503, True),
({*range(400, 403), 500, 501}, 401, True),
({*range(400, 403), 500, 501}, 405, False),
({*range(400, 403), 500, 501}, 501, True),
({*range(400, 403), 500, 501}, 503, False),
(set(), 500, False),
),
)
sentry-python-2.64.0/tests/integrations/django/ 0000775 0000000 0000000 00000000000 15220673775 0021576 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/__init__.py 0000664 0000000 0000000 00000000435 15220673775 0023711 0 ustar 00root root 0000000 0000000 import os
import sys
import pytest
pytest.importorskip("django")
# Load `django_helpers` into the module search path to test query source path names relative to module. See
# `test_query_source_with_module_in_search_path`
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
sentry-python-2.64.0/tests/integrations/django/asgi/ 0000775 0000000 0000000 00000000000 15220673775 0022521 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/asgi/__init__.py 0000664 0000000 0000000 00000000057 15220673775 0024634 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("channels")
sentry-python-2.64.0/tests/integrations/django/asgi/image.png 0000664 0000000 0000000 00000000464 15220673775 0024315 0 ustar 00root root 0000000 0000000 PNG
IHDR
IDATWcHsWT,pƃϟ+e+FQ0}^-//CfR3
VWhgVd2ܺ lzjVB!H#SM/;'15e0H6$[72iȃM32bXd;PS1KJ04`H2fÌ5b.rfO_`4;PלfŘ
M
fh@ 4 x8L IENDB` sentry-python-2.64.0/tests/integrations/django/asgi/test_asgi.py 0000664 0000000 0000000 00000076672 15220673775 0025077 0 ustar 00root root 0000000 0000000 import asyncio
import base64
import inspect
import json
import os
import sys
from unittest import mock
import django
import pytest
from channels.testing import HttpCommunicator
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.django.asgi import _asgi_middleware_mixin_factory
from tests.integrations.django.myapp.asgi import channels_application
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
APPS = [channels_application]
if django.VERSION >= (3, 0):
from tests.integrations.django.myapp.asgi import asgi_application
APPS += [asgi_application]
@pytest.fixture
def make_asgi_application():
"""Build a fresh ASGI application. Call AFTER sentry_init so middleware
instrumentation is installed before Django builds the middleware chain."""
from django.core.asgi import get_asgi_application
return get_asgi_application
@pytest.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_basic(
sentry_init,
capture_events,
capture_items,
application,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
import channels # type: ignore[import-not-found]
if span_streaming:
items = capture_items("event")
if (
sys.version_info < (3, 9)
and channels.__version__ < "4.0.0"
and django.VERSION >= (3, 0)
and django.VERSION < (4, 0)
):
# We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older
# because the async support was not really good back then and there is a known issue.
# See the TreadingIntegration for details.
with pytest.warns(UserWarning):
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
response = await comm.get_response()
await comm.wait()
else:
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(event,) = (item.payload for item in items if item.type == "event")
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
# Test that the ASGI middleware got set up correctly. Right now this needs
# to be installed manually (see myapp/asgi.py)
assert event["transaction"] == "/view-exc"
assert event["request"] == {
"cookies": {},
"headers": {},
"method": "GET",
"query_string": "test=query",
"url": "/view-exc",
}
capture_message("hi")
event = items[-1].payload
else:
events = capture_events()
if (
sys.version_info < (3, 9)
and channels.__version__ < "4.0.0"
and django.VERSION >= (3, 0)
and django.VERSION < (4, 0)
):
# We emit a UserWarning for channels 2.x and 3.x on Python 3.8 and older
# because the async support was not really good back then and there is a known issue.
# See the TreadingIntegration for details.
with pytest.warns(UserWarning):
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
response = await comm.get_response()
await comm.wait()
else:
comm = HttpCommunicator(application, "GET", "/view-exc?test=query")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
# Test that the ASGI middleware got set up correctly. Right now this needs
# to be installed manually (see myapp/asgi.py)
assert event["transaction"] == "/view-exc"
assert event["request"] == {
"cookies": {},
"headers": {},
"method": "GET",
"query_string": "test=query",
"url": "/view-exc",
}
capture_message("hi")
event = events[-1]
assert "request" not in event
@pytest.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_async_views(
sentry_init,
capture_events,
capture_items,
application,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(application, "GET", "/async_message")
if span_streaming:
items = capture_items("event")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
(event,) = events
assert event["transaction"] == "/async_message"
assert event["request"] == {
"cookies": {},
"headers": {},
"method": "GET",
"query_string": None,
"url": "/async_message",
}
@pytest.mark.parametrize("application", APPS)
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
@pytest.mark.parametrize("middleware_spans", [False, True])
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_active_thread_id(
sentry_init,
capture_envelopes,
capture_items,
teardown_profiling,
endpoint,
application,
middleware_spans,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(middleware_spans=middleware_spans)],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(application, "GET", endpoint)
if span_streaming:
with mock.patch(
"sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0
):
items = capture_items("span")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200, response["body"]
data = json.loads(response["body"])
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["is_segment"] is False:
continue
assert str(data["active"]) == span["attributes"]["thread.id"]
else:
with mock.patch(
"sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0
):
envelopes = capture_envelopes()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200, response["body"]
assert len(envelopes) == 1
profiles = [item for item in envelopes[0].items if item.type == "profile"]
assert len(profiles) == 1
data = json.loads(response["body"])
for item in profiles:
transactions = item.payload.json["transactions"]
assert len(transactions) == 1
assert str(data["active"]) == transactions[0]["active_thread_id"]
transactions = [
item for item in envelopes[0].items if item.type == "transaction"
]
assert len(transactions) == 1
for item in transactions:
transaction = item.payload.json
trace_context = transaction["contexts"]["trace"]
assert str(data["active"]) == trace_context["data"]["thread.id"]
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_async_views_concurrent_execution(
sentry_init, settings, make_asgi_application
):
import time
settings.MIDDLEWARE = []
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
application = make_asgi_application()
comm = HttpCommunicator(application, "GET", "/my_async_view") # sleeps for 1 second
comm2 = HttpCommunicator(
application, "GET", "/my_async_view"
) # sleeps for 1 second
start = time.monotonic()
resp1, resp2 = await asyncio.gather(
comm.get_response(timeout=5),
comm2.get_response(timeout=5),
)
await asyncio.gather(comm.wait(), comm2.wait())
end = time.monotonic()
assert resp1["status"] == 200
assert resp2["status"] == 200
assert (
end - start < 2
) # it takes less than 2 seconds so it was ececuting concurrently
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_async_middleware_that_is_function_concurrent_execution(
sentry_init, settings, make_asgi_application
):
import time
settings.MIDDLEWARE = [
"tests.integrations.django.myapp.middleware.simple_middleware"
]
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
application = make_asgi_application()
comm = HttpCommunicator(application, "GET", "/my_async_view") # sleeps for 1 second
comm2 = HttpCommunicator(
application, "GET", "/my_async_view"
) # sleeps for 1 second
start = time.monotonic()
resp1, resp2 = await asyncio.gather(
comm.get_response(timeout=5),
comm2.get_response(timeout=5),
)
await asyncio.gather(comm.wait(), comm2.wait())
end = time.monotonic()
assert resp1["status"] == 200
assert resp2["status"] == 200
assert (
end - start < 2
) # it takes less than 2 seconds so it was ececuting concurrently
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_async_middleware_spans(
sentry_init,
render_span_tree,
capture_events,
capture_items,
settings,
span_streaming,
make_asgi_application,
):
settings.MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"tests.integrations.django.myapp.settings.TestMiddleware",
]
sentry_init(
integrations=[DjangoIntegration(middleware_spans=True)],
traces_sample_rate=1.0,
_experiments={
"record_sql_params": True,
"trace_lifecycle": "stream" if span_streaming else "static",
},
)
application = make_asgi_application()
comm = HttpCommunicator(application, "GET", "/simple_async_view")
if span_streaming:
items = capture_items("span")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
sentry_sdk.flush()
spans = [item.payload for item in items]
# Filter out signal-receiver spans — their ordering depends on Django
# module import order and is not what this middleware test verifies.
spans = [s for s in spans if s["attributes"].get("sentry.op") != "event.django"]
assert (
render_span_tree(spans)
== """\
- sentry.op="http.server": name="/simple_async_view"
- sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.__acall__"
- sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.__acall__"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.__acall__"
- sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.__acall__"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view"
- sentry.op="view.render": name="simple_async_view\""""
)
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
(transaction,) = events
assert transaction["type"] == "transaction"
# Filter out signal-receiver spans — their ordering depends on Django
# module import order and is not what this middleware test verifies.
spans = [s for s in transaction["spans"] if s.get("op") != "event.django"]
assert (
render_span_tree(spans, transaction["contexts"]["trace"])
== """\
- op="http.server": description=null
- op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.__acall__"
- op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.__acall__"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.__acall__"
- op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.__acall__"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
- op="view.render": description="simple_async_view\""""
)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_has_trace_if_performance_enabled(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg")
if span_streaming:
items = capture_items("event", "span")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[6]["is_segment"] is True
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== spans[6]["trace_id"]
)
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(msg_event, error_event, transaction_event) = events
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_has_trace_if_performance_disabled(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg")
if span_streaming:
items = capture_items("event")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(msg_event, error_event) = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_trace_from_headers_if_performance_enabled(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
comm = HttpCommunicator(
asgi_application,
"GET",
"/view-exc-with-msg",
headers=[(b"sentry-trace", sentry_trace_header.encode())],
)
if span_streaming:
items = capture_items("event", "span")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[6]["is_segment"] is True
assert spans[6]["trace_id"] == trace_id
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(msg_event, error_event, transaction_event) = events
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_trace_from_headers_if_performance_disabled(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
comm = HttpCommunicator(
asgi_application,
"GET",
"/view-exc-with-msg",
headers=[(b"sentry-trace", sentry_trace_header.encode())],
)
if span_streaming:
items = capture_items("event")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(msg_event, error_event) = (
item.payload for item in items if item.type == "event"
)
else:
events = capture_events()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 500
(msg_event, error_event) = events
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image.png")
BODY_FORM = """--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="username"\r\n\r\nJane\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="password"\r\n\r\nhello123\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="photo"; filename="image.png"\r\nContent-Type: image/png\r\nContent-Transfer-Encoding: base64\r\n\r\n{{image_data}}\r\n--fd721ef49ea403a6--\r\n""".replace(
"{{image_data}}", base64.b64encode(open(PICTURE, "rb").read()).decode("utf-8")
).encode("utf-8")
BODY_FORM_CONTENT_LENGTH = str(len(BODY_FORM)).encode("utf-8")
@pytest.mark.parametrize("application", APPS)
@pytest.mark.parametrize(
"send_default_pii,method,headers,url_name,body,expected_data",
[
(
True,
"POST",
[(b"content-type", b"text/plain")],
"post_echo_async",
b"",
None,
),
(
True,
"POST",
[(b"content-type", b"text/plain")],
"post_echo_async",
b"some raw text body",
"",
),
(
True,
"POST",
[(b"content-type", b"application/json")],
"post_echo_async",
b'{"username":"xyz","password":"xyz"}',
{"username": "xyz", "password": "[Filtered]"},
),
(
True,
"POST",
[(b"content-type", b"application/xml")],
"post_echo_async",
b'',
"",
),
(
True,
"POST",
[
(b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"),
(b"content-length", BODY_FORM_CONTENT_LENGTH),
],
"post_echo_async",
BODY_FORM,
{"password": "[Filtered]", "photo": "", "username": "Jane"},
),
(
False,
"POST",
[(b"content-type", b"text/plain")],
"post_echo_async",
b"",
None,
),
(
False,
"POST",
[(b"content-type", b"text/plain")],
"post_echo_async",
b"some raw text body",
"",
),
(
False,
"POST",
[(b"content-type", b"application/json")],
"post_echo_async",
b'{"username":"xyz","password":"xyz"}',
{"username": "xyz", "password": "[Filtered]"},
),
(
False,
"POST",
[(b"content-type", b"application/xml")],
"post_echo_async",
b'',
"",
),
(
False,
"POST",
[
(b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"),
(b"content-length", BODY_FORM_CONTENT_LENGTH),
],
"post_echo_async",
BODY_FORM,
{"password": "[Filtered]", "photo": "", "username": "Jane"},
),
],
)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_asgi_request_body(
sentry_init,
capture_envelopes,
capture_items,
application,
send_default_pii,
method,
headers,
url_name,
body,
expected_data,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=send_default_pii,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(
application,
method=method,
headers=headers,
path=reverse(url_name),
body=body,
)
if span_streaming:
items = capture_items("event")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
assert response["body"] == body
sentry_sdk.flush()
(event,) = (item.payload for item in items if item.type == "event")
else:
envelopes = capture_envelopes()
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
assert response["body"] == body
(envelope,) = envelopes
event = envelope.get_event()
if expected_data is not None:
assert event["request"]["data"] == expected_data
else:
assert "data" not in event["request"]
@pytest.mark.asyncio
@pytest.mark.skipif(
sys.version_info >= (3, 12),
reason=(
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
),
)
@pytest.mark.skipif(
django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0"
)
async def test_asgi_mixin_iscoroutinefunction_before_3_12():
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
async def get_response(): ...
instance = sentry_asgi_mixin(get_response)
assert asyncio.iscoroutinefunction(instance)
@pytest.mark.skipif(
sys.version_info >= (3, 12),
reason=(
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
),
)
def test_asgi_mixin_iscoroutinefunction_when_not_async_before_3_12():
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
def get_response(): ...
instance = sentry_asgi_mixin(get_response)
assert not asyncio.iscoroutinefunction(instance)
@pytest.mark.asyncio
@pytest.mark.skipif(
sys.version_info < (3, 12),
reason=(
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
),
)
async def test_asgi_mixin_iscoroutinefunction_after_3_12():
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
async def get_response(): ...
instance = sentry_asgi_mixin(get_response)
assert inspect.iscoroutinefunction(instance)
@pytest.mark.skipif(
sys.version_info < (3, 12),
reason=(
"asyncio.iscoroutinefunction has been replaced in 3.12 by inspect.iscoroutinefunction"
),
)
def test_asgi_mixin_iscoroutinefunction_when_not_async_after_3_12():
sentry_asgi_mixin = _asgi_middleware_mixin_factory(lambda: None)
def get_response(): ...
instance = sentry_asgi_mixin(get_response)
assert not inspect.iscoroutinefunction(instance)
@pytest.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_async_view(
sentry_init,
capture_events,
capture_items,
application,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
comm = HttpCommunicator(application, "GET", "/simple_async_view")
if span_streaming:
items = capture_items("span")
await comm.get_response()
await comm.wait()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[5]["name"] == "/simple_async_view"
else:
events = capture_events()
await comm.get_response()
await comm.wait()
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "/simple_async_view"
@pytest.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_transaction_http_method_default(
sentry_init,
capture_events,
capture_items,
application,
span_streaming,
):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
comm = HttpCommunicator(application, "GET", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "HEAD", "/simple_async_view")
await comm.get_response()
await comm.wait()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[5]["attributes"]["http.request.method"] == "GET"
else:
events = capture_events()
comm = HttpCommunicator(application, "GET", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "HEAD", "/simple_async_view")
await comm.get_response()
await comm.wait()
(event,) = events
assert len(events) == 1
assert event["request"]["method"] == "GET"
@pytest.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.skipif(
django.VERSION < (3, 0), reason="Django ASGI support shipped in 3.0"
)
@pytest.mark.parametrize("span_streaming", [True, False])
async def test_transaction_http_method_custom(
sentry_init,
capture_events,
capture_items,
application,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
comm = HttpCommunicator(application, "GET", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "HEAD", "/simple_async_view")
await comm.get_response()
await comm.wait()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[10]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS"
assert spans[16]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD"
else:
events = capture_events()
comm = HttpCommunicator(application, "GET", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "OPTIONS", "/simple_async_view")
await comm.get_response()
await comm.wait()
comm = HttpCommunicator(application, "HEAD", "/simple_async_view")
await comm.get_response()
await comm.wait()
assert len(events) == 2
(event1, event2) = events
assert event1["request"]["method"] == "OPTIONS"
assert event2["request"]["method"] == "HEAD"
sentry-python-2.64.0/tests/integrations/django/django_helpers/ 0000775 0000000 0000000 00000000000 15220673775 0024562 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/django_helpers/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0026661 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/django_helpers/views.py 0000664 0000000 0000000 00000000456 15220673775 0026276 0 ustar 00root root 0000000 0000000 from django.contrib.auth.models import User
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def postgres_select_orm(request, *args, **kwargs):
user = User.objects.using("postgres").all().first()
return HttpResponse("ok {}".format(user))
sentry-python-2.64.0/tests/integrations/django/myapp/ 0000775 0000000 0000000 00000000000 15220673775 0022724 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0025023 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/asgi.py 0000664 0000000 0000000 00000000750 15220673775 0024223 0 ustar 00root root 0000000 0000000 """
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "tests.integrations.django.myapp.settings"
)
django.setup()
channels_application = get_default_application()
if django.VERSION >= (3, 0):
from django.core.asgi import get_asgi_application
asgi_application = get_asgi_application()
sentry-python-2.64.0/tests/integrations/django/myapp/custom_urls.py 0000664 0000000 0000000 00000001724 15220673775 0025661 0 ustar 00root root 0000000 0000000 """myapp URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
try:
from django.urls import path
except ImportError:
from django.conf.urls import url
def path(path, *args, **kwargs):
return url("^{}$".format(path), *args, **kwargs)
from . import views
urlpatterns = [
path("custom/ok", views.custom_ok, name="custom_ok"),
path("custom/exc", views.custom_exc, name="custom_exc"),
]
sentry-python-2.64.0/tests/integrations/django/myapp/manage.py 0000664 0000000 0000000 00000000434 15220673775 0024527 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "tests.integrations.django.myapp.settings"
)
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
sentry-python-2.64.0/tests/integrations/django/myapp/management/ 0000775 0000000 0000000 00000000000 15220673775 0025040 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/management/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0027137 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/management/commands/ 0000775 0000000 0000000 00000000000 15220673775 0026641 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/management/commands/__init__.py 0000664 0000000 0000000 00000000000 15220673775 0030740 0 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/management/commands/mycrash.py 0000664 0000000 0000000 00000000273 15220673775 0030663 0 ustar 00root root 0000000 0000000 from django.core.management.base import BaseCommand
class Command(BaseCommand):
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
1 / 0
sentry-python-2.64.0/tests/integrations/django/myapp/middleware.py 0000664 0000000 0000000 00000001421 15220673775 0025411 0 ustar 00root root 0000000 0000000 import django
if django.VERSION >= (3, 1):
import asyncio
from django.utils.decorators import sync_and_async_middleware
@sync_and_async_middleware
def simple_middleware(get_response):
if asyncio.iscoroutinefunction(get_response):
async def middleware(request):
response = await get_response(request)
return response
else:
def middleware(request):
response = get_response(request)
return response
return middleware
def custom_urlconf_middleware(get_response):
def middleware(request):
request.urlconf = "tests.integrations.django.myapp.custom_urls"
response = get_response(request)
return response
return middleware
sentry-python-2.64.0/tests/integrations/django/myapp/routing.py 0000664 0000000 0000000 00000000756 15220673775 0024775 0 ustar 00root root 0000000 0000000 import channels
from channels.routing import ProtocolTypeRouter
try:
from channels.http import AsgiHandler
if channels.__version__ < "3.0.0":
django_asgi_app = AsgiHandler
else:
django_asgi_app = AsgiHandler()
except ModuleNotFoundError:
# Since channels 4.0 ASGI handling is done by Django itself
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({"http": django_asgi_app})
sentry-python-2.64.0/tests/integrations/django/myapp/settings.py 0000664 0000000 0000000 00000011675 15220673775 0025150 0 ustar 00root root 0000000 0000000 """
Django settings for myapp project.
Generated by 'django-admin startproject' using Django 2.0.7.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
# We shouldn't access settings while setting up integrations. Initialize SDK
# here to provoke any errors that might occur.
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(integrations=[DjangoIntegration()])
import os
from django.utils.deprecation import MiddlewareMixin
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "u95e#xr$t3!vdux)fj11!*q*^w^^r#kiyrvt3kjui-t_k%m3op"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["localhost"]
# Application definition
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"tests.integrations.django.myapp",
]
class TestMiddleware(MiddlewareMixin):
def process_request(self, request):
# https://github.com/getsentry/sentry-python/issues/837 -- We should
# not touch the resolver_match because apparently people rely on it.
if request.resolver_match:
assert not getattr(request.resolver_match.callback, "__wrapped__", None)
if "middleware-exc" in request.path:
1 / 0
def process_response(self, request, response):
return response
def TestFunctionMiddleware(get_response): # noqa: N802
def middleware(request):
return get_response(request)
return middleware
MIDDLEWARE_CLASSES = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"tests.integrations.django.myapp.settings.TestMiddleware",
]
if MiddlewareMixin is not object:
MIDDLEWARE = MIDDLEWARE_CLASSES + [
"tests.integrations.django.myapp.settings.TestFunctionMiddleware"
]
ROOT_URLCONF = "tests.integrations.django.myapp.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"debug": True,
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
}
]
WSGI_APPLICATION = "tests.integrations.django.myapp.wsgi.application"
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
try:
import psycopg2 # noqa
db_engine = "django.db.backends.postgresql"
try:
from django.db.backends import postgresql # noqa: F401
except ImportError:
db_engine = "django.db.backends.postgresql_psycopg2"
DATABASES["postgres"] = {
"ENGINE": db_engine,
"HOST": os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"),
"PORT": int(os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432")),
"USER": os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_USER", "postgres"),
"PASSWORD": os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_PASSWORD", "sentry"),
"NAME": os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_NAME", f"myapp_db_{os.getpid()}"
),
}
except (ImportError, KeyError):
from sentry_sdk.utils import logger
logger.warning("No psycopg2 found, testing with SQLite.")
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = False
TEMPLATE_DEBUG = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = "/static/"
# django-channels specific
ASGI_APPLICATION = "tests.integrations.django.myapp.routing.application"
sentry-python-2.64.0/tests/integrations/django/myapp/signals.py 0000664 0000000 0000000 00000000567 15220673775 0024746 0 ustar 00root root 0000000 0000000 from django.core import signals
from django.dispatch import receiver
myapp_custom_signal = signals.Signal()
myapp_custom_signal_silenced = signals.Signal()
@receiver(myapp_custom_signal)
def signal_handler(sender, **kwargs):
assert sender == "hello"
@receiver(myapp_custom_signal_silenced)
def signal_handler_silenced(sender, **kwargs):
assert sender == "hello"
sentry-python-2.64.0/tests/integrations/django/myapp/templates/ 0000775 0000000 0000000 00000000000 15220673775 0024722 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/django/myapp/templates/error.html 0000664 0000000 0000000 00000000113 15220673775 0026734 0 ustar 00root root 0000000 0000000 1
2
3
4
5
6
7
8
9
{% invalid template tag %}
11
12
13
14
15
16
17
18
19
20
sentry-python-2.64.0/tests/integrations/django/myapp/templates/trace_meta.html 0000664 0000000 0000000 00000000030 15220673775 0027705 0 ustar 00root root 0000000 0000000 {{ sentry_trace_meta }}
sentry-python-2.64.0/tests/integrations/django/myapp/templates/user_name.html 0000664 0000000 0000000 00000000043 15220673775 0027563 0 ustar 00root root 0000000 0000000 {{ request.user }}: {{ user_age }}
sentry-python-2.64.0/tests/integrations/django/myapp/urls.py 0000664 0000000 0000000 00000012754 15220673775 0024274 0 ustar 00root root 0000000 0000000 """myapp URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
try:
from django.urls import path
except ImportError:
from django.conf.urls import url
def path(path, *args, **kwargs):
return url("^{}$".format(path), *args, **kwargs)
from django_helpers import views as helper_views
from . import views
urlpatterns = [
path("view-exc", views.view_exc, name="view_exc"),
path("view-exc-with-msg", views.view_exc_with_msg, name="view_exc_with_msg"),
path("cached-view", views.cached_view, name="cached_view"),
path("not-cached-view", views.not_cached_view, name="not_cached_view"),
path(
"view-with-cached-template-fragment",
views.view_with_cached_template_fragment,
name="view_with_cached_template_fragment",
),
path(
"read-body-and-view-exc",
views.read_body_and_view_exc,
name="read_body_and_view_exc",
),
path("middleware-exc", views.message, name="middleware_exc"),
path("message", views.message, name="message"),
path("nomessage", views.nomessage, name="nomessage"),
path("view-with-signal", views.view_with_signal, name="view_with_signal"),
path("mylogin", views.mylogin, name="mylogin"),
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
path("sentryclass", views.SentryClassBasedView(), name="sentryclass"),
path(
"sentryclass-csrf",
views.SentryClassBasedViewWithCsrf(),
name="sentryclass_csrf",
),
path("post-echo", views.post_echo, name="post_echo"),
path("template-exc", views.template_exc, name="template_exc"),
path("template-test", views.template_test, name="template_test"),
path("template-test2", views.template_test2, name="template_test2"),
path("template-test3", views.template_test3, name="template_test3"),
path("template-test4", views.template_test4, name="template_test4"),
path("postgres-select", views.postgres_select, name="postgres_select"),
path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"),
path(
"postgres-insert-no-autocommit",
views.postgres_insert_orm_no_autocommit,
name="postgres_insert_orm_no_autocommit",
),
path(
"postgres-insert-no-autocommit-rollback",
views.postgres_insert_orm_no_autocommit_rollback,
name="postgres_insert_orm_no_autocommit_rollback",
),
path(
"postgres-insert-atomic",
views.postgres_insert_orm_atomic,
name="postgres_insert_orm_atomic",
),
path(
"postgres-insert-atomic-rollback",
views.postgres_insert_orm_atomic_rollback,
name="postgres_insert_orm_atomic_rollback",
),
path(
"postgres-insert-atomic-exception",
views.postgres_insert_orm_atomic_exception,
name="postgres_insert_orm_atomic_exception",
),
path(
"postgres-select-slow-from-supplement",
helper_views.postgres_select_orm,
name="postgres_select_slow_from_supplement",
),
path(
"permission-denied-exc",
views.permission_denied_exc,
name="permission_denied_exc",
),
path(
"csrf-hello-not-exempt",
views.csrf_hello_not_exempt,
name="csrf_hello_not_exempt",
),
path("sync/thread_ids", views.thread_ids_sync, name="thread_ids_sync"),
path(
"send-myapp-custom-signal",
views.send_myapp_custom_signal,
name="send_myapp_custom_signal",
),
]
# async views
if views.async_message is not None:
urlpatterns.append(path("async_message", views.async_message, name="async_message"))
if views.my_async_view is not None:
urlpatterns.append(path("my_async_view", views.my_async_view, name="my_async_view"))
if views.my_async_view is not None:
urlpatterns.append(
path("simple_async_view", views.simple_async_view, name="simple_async_view")
)
if views.thread_ids_async is not None:
urlpatterns.append(
path("async/thread_ids", views.thread_ids_async, name="thread_ids_async")
)
if views.post_echo_async is not None:
urlpatterns.append(
path("post_echo_async", views.post_echo_async, name="post_echo_async")
)
# rest framework
try:
urlpatterns.append(
path("rest-framework-exc", views.rest_framework_exc, name="rest_framework_exc")
)
urlpatterns.append(
path(
"rest-framework-read-body-and-exc",
views.rest_framework_read_body_and_exc,
name="rest_framework_read_body_and_exc",
)
)
urlpatterns.append(path("rest-hello", views.rest_hello, name="rest_hello"))
urlpatterns.append(
path("rest-json-response", views.rest_json_response, name="rest_json_response")
)
urlpatterns.append(
path(
"rest-permission-denied-exc",
views.rest_permission_denied_exc,
name="rest_permission_denied_exc",
)
)
except AttributeError:
pass
handler500 = views.handler500
handler404 = views.handler404
sentry-python-2.64.0/tests/integrations/django/myapp/views.py 0000664 0000000 0000000 00000022066 15220673775 0024441 0 ustar 00root root 0000000 0000000 import asyncio
import json
import threading
from django.contrib.auth import login
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.dispatch import Signal
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
from django.shortcuts import render
from django.template import Context, Template
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView
from tests.integrations.django.myapp.signals import (
myapp_custom_signal,
myapp_custom_signal_silenced,
)
try:
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view(["POST"])
def rest_framework_exc(request):
1 / 0
@api_view(["POST"])
def rest_framework_read_body_and_exc(request):
request.data
1 / 0
@api_view(["GET"])
def rest_hello(request):
return HttpResponse("ok")
@api_view(["GET"])
def rest_permission_denied_exc(request):
raise PermissionDenied("bye")
@api_view(["GET"])
def rest_json_response(request):
return Response(dict(ok=True))
except ImportError:
pass
import sentry_sdk
from sentry_sdk import capture_message
@csrf_exempt
def view_exc(request):
1 / 0
@csrf_exempt
def view_exc_with_msg(request):
capture_message("oops")
1 / 0
@cache_page(60)
def cached_view(request):
return HttpResponse("ok")
def not_cached_view(request):
return HttpResponse("ok")
def view_with_cached_template_fragment(request):
template = Template(
"""{% load cache %}
Not cached content goes here.
{% cache 500 some_identifier %}
And here some cached content.
{% endcache %}
"""
)
rendered = template.render(Context({}))
return HttpResponse(rendered)
# This is a "class based view" as previously found in the sentry codebase. The
# interesting property of this one is that csrf_exempt, as a class attribute,
# is not in __dict__, so regular use of functools.wraps will not forward the
# attribute.
class SentryClassBasedView:
csrf_exempt = True
def __call__(self, request):
return HttpResponse("ok")
class SentryClassBasedViewWithCsrf:
def __call__(self, request):
return HttpResponse("ok")
@csrf_exempt
def read_body_and_view_exc(request):
request.read()
1 / 0
@csrf_exempt
def message(request):
sentry_sdk.capture_message("hi")
return HttpResponse("ok")
@csrf_exempt
def nomessage(request):
return HttpResponse("ok")
@csrf_exempt
def view_with_signal(request):
custom_signal = Signal()
custom_signal.send(sender="hello")
return HttpResponse("ok")
@csrf_exempt
def mylogin(request):
user = User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
user.backend = "django.contrib.auth.backends.ModelBackend"
login(request, user)
return HttpResponse("ok")
@csrf_exempt
def handler500(request):
return HttpResponseServerError("Sentry error.")
class ClassBasedView(ListView):
model = None
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def head(self, *args, **kwargs):
sentry_sdk.capture_message("hi")
return HttpResponse("")
def post(self, *args, **kwargs):
return HttpResponse("ok")
@csrf_exempt
def post_echo(request):
sentry_sdk.capture_message("hi")
return HttpResponse(request.body)
@csrf_exempt
def handler404(*args, **kwargs):
sentry_sdk.capture_message("not found", level="error")
return HttpResponseNotFound("404")
@csrf_exempt
def template_exc(request, *args, **kwargs):
return render(request, "error.html")
@csrf_exempt
def template_test(request, *args, **kwargs):
return render(request, "user_name.html", {"user_age": 20})
@csrf_exempt
def custom_ok(request, *args, **kwargs):
return HttpResponse("custom ok")
@csrf_exempt
def custom_exc(request, *args, **kwargs):
1 / 0
@csrf_exempt
def template_test2(request, *args, **kwargs):
return TemplateResponse(
request, ("user_name.html", "another_template.html"), {"user_age": 25}
)
@csrf_exempt
def template_test3(request, *args, **kwargs):
traceparent = sentry_sdk.get_current_scope().get_traceparent()
if traceparent is None:
traceparent = sentry_sdk.get_isolation_scope().get_traceparent()
baggage = sentry_sdk.get_current_scope().get_baggage()
if baggage is None:
baggage = sentry_sdk.get_isolation_scope().get_baggage()
capture_message(traceparent + "\n" + baggage.serialize())
return render(request, "trace_meta.html", {})
@csrf_exempt
def template_test4(request, *args, **kwargs):
User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
my_queryset = User.objects.all() # noqa
template_context = {
"user_age": 25,
"complex_context": my_queryset,
"complex_list": [1, 2, 3, my_queryset],
"complex_dict": {
"a": 1,
"d": my_queryset,
},
"none_context": None,
}
return TemplateResponse(
request,
"user_name.html",
template_context,
)
@csrf_exempt
def postgres_select(request, *args, **kwargs):
from django.db import connections
cursor = connections["postgres"].cursor()
cursor.execute("SELECT 1;")
return HttpResponse("ok")
@csrf_exempt
def postgres_select_orm(request, *args, **kwargs):
user = User.objects.using("postgres").all().first()
return HttpResponse("ok {}".format(user))
@csrf_exempt
def postgres_insert_orm_no_autocommit(request, *args, **kwargs):
transaction.set_autocommit(False, using="postgres")
try:
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
transaction.commit(using="postgres")
except Exception:
transaction.rollback(using="postgres")
transaction.set_autocommit(True, using="postgres")
raise
transaction.set_autocommit(True, using="postgres")
return HttpResponse("ok {}".format(user))
@csrf_exempt
def postgres_insert_orm_no_autocommit_rollback(request, *args, **kwargs):
transaction.set_autocommit(False, using="postgres")
try:
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
transaction.rollback(using="postgres")
except Exception:
transaction.rollback(using="postgres")
transaction.set_autocommit(True, using="postgres")
raise
transaction.set_autocommit(True, using="postgres")
return HttpResponse("ok {}".format(user))
@csrf_exempt
def postgres_insert_orm_atomic(request, *args, **kwargs):
with transaction.atomic(using="postgres"):
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
return HttpResponse("ok {}".format(user))
@csrf_exempt
def postgres_insert_orm_atomic_rollback(request, *args, **kwargs):
with transaction.atomic(using="postgres"):
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
transaction.set_rollback(True, using="postgres")
return HttpResponse("ok {}".format(user))
@csrf_exempt
def postgres_insert_orm_atomic_exception(request, *args, **kwargs):
try:
with transaction.atomic(using="postgres"):
user = User.objects.db_manager("postgres").create_user(
username="user1",
)
transaction.set_rollback(True, using="postgres")
1 / 0
except ZeroDivisionError:
pass
return HttpResponse("ok {}".format(user))
@csrf_exempt
def permission_denied_exc(*args, **kwargs):
raise PermissionDenied("bye")
def csrf_hello_not_exempt(*args, **kwargs):
return HttpResponse("ok")
def thread_ids_sync(*args, **kwargs):
response = json.dumps(
{
"main": threading.main_thread().ident,
"active": threading.current_thread().ident,
}
)
return HttpResponse(response)
async def async_message(request):
sentry_sdk.capture_message("hi")
return HttpResponse("ok")
async def my_async_view(request):
await asyncio.sleep(1)
return HttpResponse("Hello World")
async def simple_async_view(request):
return HttpResponse("Simple Hello World")
async def thread_ids_async(request):
response = json.dumps(
{
"main": threading.main_thread().ident,
"active": threading.current_thread().ident,
}
)
return HttpResponse(response)
async def post_echo_async(request):
sentry_sdk.capture_message("hi")
return HttpResponse(request.body)
post_echo_async.csrf_exempt = True
@csrf_exempt
def send_myapp_custom_signal(request):
myapp_custom_signal.send(sender="hello")
myapp_custom_signal_silenced.send(sender="hello")
return HttpResponse("ok")
sentry-python-2.64.0/tests/integrations/django/myapp/wsgi.py 0000664 0000000 0000000 00000000643 15220673775 0024252 0 ustar 00root root 0000000 0000000 """
WSGI config for myapp project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "tests.integrations.django.myapp.settings"
)
application = get_wsgi_application()
sentry-python-2.64.0/tests/integrations/django/test_basic.py 0000664 0000000 0000000 00000230203 15220673775 0024270 0 ustar 00root root 0000000 0000000 import inspect
import json
import os
import re
import sys
from functools import partial
from unittest.mock import patch
import pytest
from django import VERSION as DJANGO_VERSION
from django.contrib.auth.models import User
from django.core.management import execute_from_command_line
from django.db.utils import DataError, OperationalError, ProgrammingError
from django.http.request import RawPostDataException
from django.template.context import make_context
from django.utils.functional import SimpleLazyObject
from werkzeug.test import Client
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
import sentry_sdk
from sentry_sdk import capture_exception, capture_message
from sentry_sdk._compat import PY310
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.django import (
DjangoIntegration,
DjangoRequestExtractor,
_set_db_data,
)
from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name
from sentry_sdk.integrations.executing import ExecutingIntegration
from sentry_sdk.profiler.utils import get_frame_name
from sentry_sdk.tracing import Span
from tests.conftest import unpack_werkzeug_response
from tests.integrations.django.myapp.signals import myapp_custom_signal_silenced
from tests.integrations.django.myapp.wsgi import application
from tests.integrations.django.utils import pytest_mark_django_db_decorator
DJANGO_VERSION = DJANGO_VERSION[:2]
@pytest.fixture
def client():
return Client(application)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_view_exceptions(
sentry_init,
client,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
exceptions = capture_exceptions()
if span_streaming:
items = capture_items("event")
client.get(reverse("view_exc"))
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
client.get(reverse("view_exc"))
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "django"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django(
sentry_init,
client,
capture_exceptions,
capture_events,
capture_items,
settings,
span_streaming,
):
"""
Test that ensures if django settings.USE_X_FORWARDED_HOST is set to True
then the SDK sets the request url to the `HTTP_X_FORWARDED_FOR`
"""
settings.USE_X_FORWARDED_HOST = True
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
exceptions = capture_exceptions()
if span_streaming:
items = capture_items("event")
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
assert event["request"]["url"] == "http://example.com/view-exc"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_ensures_x_forwarded_header_is_not_honored_when_unenabled_in_django(
sentry_init,
client,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
"""
Test that ensures if django settings.USE_X_FORWARDED_HOST is set to False
then the SDK sets the request url to the `HTTP_POST`
"""
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
exceptions = capture_exceptions()
if span_streaming:
items = capture_items("event")
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
client.get(reverse("view_exc"), headers={"X_FORWARDED_HOST": "example.com"})
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
assert event["request"]["url"] == "http://localhost/view-exc"
def test_middleware_exceptions(sentry_init, client, capture_exceptions):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
exceptions = capture_exceptions()
client.get(reverse("middleware_exc"))
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_request_captured(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event")
content, status, headers = unpack_werkzeug_response(
client.get(reverse("message"))
)
assert content == b"ok"
(event,) = (item.payload for item in items if item.type == "event")
assert event["transaction"] == "/message"
assert event["request"] == {
"cookies": {},
"env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
"headers": {"Host": "localhost"},
"method": "GET",
"query_string": "",
"url": "http://localhost/message",
}
else:
events = capture_events()
content, status, headers = unpack_werkzeug_response(
client.get(reverse("message"))
)
assert content == b"ok"
(event,) = events
assert event["transaction"] == "/message"
assert event["request"] == {
"cookies": {},
"env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
"headers": {"Host": "localhost"},
"method": "GET",
"query_string": "",
"url": "http://localhost/message",
}
@pytest.mark.parametrize("span_streaming", [True, False])
def test_transaction_with_class_view(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(transaction_style="function_name")],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event")
content, status, headers = unpack_werkzeug_response(
client.head(reverse("classbased"))
)
assert status.lower() == "200 ok"
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
content, status, headers = unpack_werkzeug_response(
client.head(reverse("classbased"))
)
assert status.lower() == "200 ok"
(event,) = events
assert (
event["transaction"] == "tests.integrations.django.myapp.views.ClassBasedView"
)
assert event["message"] == "hi"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_has_trace_if_performance_enabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "span")
client.head(reverse("view_exc_with_msg"))
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[3]["is_segment"] is True
assert "trace_id" in spans[3]
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== spans[3]["trace_id"]
)
else:
events = capture_events()
client.head(reverse("view_exc_with_msg"))
(msg_event, error_event, transaction_event) = events
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_has_trace_if_performance_disabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event")
client.head(reverse("view_exc_with_msg"))
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
client.head(reverse("view_exc_with_msg"))
(msg_event, error_event) = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_trace_from_headers_if_performance_enabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
if span_streaming:
items = capture_items("event", "span")
client.head(
reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header}
)
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert spans[3]["is_segment"] is True
assert "trace_id" in spans[3]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert spans[3]["trace_id"] == trace_id
else:
events = capture_events()
client.head(
reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header}
)
(msg_event, error_event, transaction_event) = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert transaction_event["contexts"]["trace"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.parametrize("span_streaming", [True, False])
def test_trace_from_headers_if_performance_disabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
if span_streaming:
items = capture_items("event")
client.head(
reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header}
)
(
msg_event,
error_event,
) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
client.head(
reverse("view_exc_with_msg"), headers={"sentry-trace": sentry_trace_header}
)
(msg_event, error_event) = events
assert msg_event["contexts"]["trace"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert error_event["contexts"]["trace"]
assert "trace_id" in error_event["contexts"]["trace"]
assert msg_event["contexts"]["trace"]["trace_id"] == trace_id
assert error_event["contexts"]["trace"]["trace_id"] == trace_id
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_user_captured(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event")
content, status, headers = unpack_werkzeug_response(
client.get(reverse("mylogin"))
)
assert content == b"ok"
sentry_sdk.flush()
assert not items
content, status, headers = unpack_werkzeug_response(
client.get(reverse("message"))
)
assert content == b"ok"
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
content, status, headers = unpack_werkzeug_response(
client.get(reverse("mylogin"))
)
assert content == b"ok"
assert not events
content, status, headers = unpack_werkzeug_response(
client.get(reverse("message"))
)
assert content == b"ok"
(event,) = events
assert event["user"] == {
"email": "lennon@thebeatles.com",
"username": "john",
"id": "1",
}
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_materialized_user_captured(
sentry_init,
client,
capture_events,
capture_items,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
content, status, headers = unpack_werkzeug_response(client.get(reverse("mylogin")))
assert content == b"ok"
items = capture_items("span")
content, status, headers = unpack_werkzeug_response(
client.get(reverse("template_test"))
)
sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = (span for span in spans if span["name"] == "/template-test")
assert span["attributes"][SPANDATA.USER_ID] == "1"
assert span["attributes"][SPANDATA.USER_EMAIL] == "lennon@thebeatles.com"
assert span["attributes"][SPANDATA.USER_NAME] == "john"
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_queryset_repr(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
if span_streaming:
items = capture_items("event")
try:
my_queryset = User.objects.all() # noqa
1 / 0
except Exception:
capture_exception()
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
try:
my_queryset = User.objects.all() # noqa
1 / 0
except Exception:
capture_exception()
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
(frame,) = exception["stacktrace"]["frames"]
assert frame["vars"]["my_queryset"].startswith(
"\n',
rendered_meta,
)
assert match is not None
assert match.group(1) == traceparent
rendered_baggage = match.group(2)
assert rendered_baggage == baggage
@pytest.mark.parametrize("with_executing_integration", [[], [ExecutingIntegration()]])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_template_exception(
sentry_init,
client,
capture_events,
capture_items,
with_executing_integration,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()] + with_executing_integration,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event")
content, status, headers = unpack_werkzeug_response(
client.get(reverse("template_exc"))
)
assert status.lower() == "500 internal server error"
(event,) = (item.payload for item in items if item.type == "event")
else:
events = capture_events()
content, status, headers = unpack_werkzeug_response(
client.get(reverse("template_exc"))
)
assert status.lower() == "500 internal server error"
(event,) = events
exception = event["exception"]["values"][-1]
assert exception["type"] == "TemplateSyntaxError"
frames = [
f
for f in exception["stacktrace"]["frames"]
if not f["filename"].startswith("django/")
]
view_frame, template_frame = frames[-2:]
assert template_frame["context_line"] == "{% invalid template tag %}\n"
assert template_frame["pre_context"] == ["5\n", "6\n", "7\n", "8\n", "9\n"]
assert template_frame["post_context"] == ["11\n", "12\n", "13\n", "14\n", "15\n"]
assert template_frame["lineno"] == 10
assert template_frame["filename"].endswith("error.html")
filenames = [
(f.get("function"), f.get("module")) for f in exception["stacktrace"]["frames"]
]
if with_executing_integration:
assert filenames[-3:] == [
("Parser.parse", "django.template.base"),
(None, None),
("Parser.invalid_block_tag", "django.template.base"),
]
else:
assert filenames[-3:] == [
("parse", "django.template.base"),
(None, None),
("invalid_block_tag", "django.template.base"),
]
@pytest.mark.parametrize(
"route", ["rest_framework_exc", "rest_framework_read_body_and_exc"]
)
@pytest.mark.parametrize(
"ct,body",
[
["application/json", {"foo": "bar"}],
["application/json", 1],
["application/json", "foo"],
["application/x-www-form-urlencoded", {"foo": "bar"}],
],
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_rest_framework_basic(
sentry_init,
client,
capture_events,
capture_items,
capture_exceptions,
ct,
body,
route,
span_streaming,
):
pytest.importorskip("rest_framework")
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
exceptions = capture_exceptions()
items = capture_items("event")
if ct == "application/json":
client.post(
reverse(route), data=json.dumps(body), content_type="application/json"
)
elif ct == "application/x-www-form-urlencoded":
client.post(reverse(route), data=body)
else:
raise AssertionError("unreachable")
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = (item.payload for item in items if item.type == "event")
else:
exceptions = capture_exceptions()
events = capture_events()
if ct == "application/json":
client.post(
reverse(route), data=json.dumps(body), content_type="application/json"
)
elif ct == "application/x-www-form-urlencoded":
client.post(reverse(route), data=body)
else:
raise AssertionError("unreachable")
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "django"
assert event["request"]["data"] == body
assert event["request"]["headers"]["Content-Type"] == ct
@pytest.mark.parametrize(
"endpoint", ["rest_permission_denied_exc", "permission_denied_exc"]
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_does_not_capture_403(
sentry_init,
client,
capture_events,
capture_items,
endpoint,
span_streaming,
):
if endpoint == "rest_permission_denied_exc":
pytest.importorskip("rest_framework")
sentry_init(
integrations=[DjangoIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "transaction", "span")
_, status, _ = unpack_werkzeug_response(client.get(reverse(endpoint)))
assert status.lower() == "403 forbidden"
assert not items
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(client.get(reverse(endpoint)))
assert status.lower() == "403 forbidden"
assert not events
@pytest.mark.parametrize("span_streaming", [True, False])
def test_render_spans(
sentry_init,
client,
capture_events,
capture_items,
render_span_tree,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
views_tests = [
(
reverse("template_test2"),
'- sentry.op="template.render": name="[user_name.html, ...]"',
),
]
if DJANGO_VERSION >= (1, 7):
views_tests.append(
(
reverse("template_test"),
'- sentry.op="template.render": name="user_name.html"',
),
)
for url, expected_line in views_tests:
items = capture_items("span")
client.get(url)
sentry_sdk.flush()
spans = [item.payload for item in items]
assert expected_line in render_span_tree(spans)
else:
views_tests = [
(
reverse("template_test2"),
'- op="template.render": description="[user_name.html, ...]"',
),
]
if DJANGO_VERSION >= (1, 7):
views_tests.append(
(
reverse("template_test"),
'- op="template.render": description="user_name.html"',
),
)
for url, expected_line in views_tests:
events = capture_events()
client.get(url)
transaction = events[0]
assert expected_line in render_span_tree(
transaction["spans"], transaction["contexts"]["trace"]
)
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_render_spans_queryset_in_data(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("template_test4"))
(transaction,) = events
template_context = transaction["spans"][-1]["data"]["context"]
assert template_context["user_age"] == 25
assert template_context["complex_context"].startswith(
"= (1, 10):
EXPECTED_MIDDLEWARE_SPANS = """\
- sentry.op="http.server": name="/message"
- sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.__call__"
- sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.__call__"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.__call__"
- sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.__call__"
- sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view"
- sentry.op="view.render": name="message"\
"""
else:
EXPECTED_MIDDLEWARE_SPANS = """\
- sentry.op="http.server": name="/message"
- sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.process_request"
- sentry.op="middleware.django": name="django.contrib.auth.middleware.AuthenticationMiddleware.process_request"
- sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.process_request"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_view"
- sentry.op="view.render": name="message"
- sentry.op="middleware.django": name="tests.integrations.django.myapp.settings.TestMiddleware.process_response"
- sentry.op="middleware.django": name="django.middleware.csrf.CsrfViewMiddleware.process_response"
- sentry.op="middleware.django": name="django.contrib.sessions.middleware.SessionMiddleware.process_response"\
"""
assert render_span_tree(spans) == EXPECTED_MIDDLEWARE_SPANS
else:
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
if DJANGO_VERSION >= (1, 10):
EXPECTED_MIDDLEWARE_SPANS = """\
- op="http.server": description=null
- op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.__call__"
- op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.__call__"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.__call__"
- op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.__call__"
- op="middleware.django": description="tests.integrations.django.myapp.settings.TestFunctionMiddleware.__call__"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
- op="view.render": description="message"\
"""
else:
EXPECTED_MIDDLEWARE_SPANS = """\
- op="http.server": description=null
- op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_request"
- op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.process_request"
- op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.process_request"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
- op="view.render": description="message"
- op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.process_response"
- op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_response"
- op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_response"\
"""
assert (
render_span_tree(transaction["spans"], transaction["contexts"]["trace"])
== EXPECTED_MIDDLEWARE_SPANS
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_middleware_spans_disabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(signals_spans=False),
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "span")
client.get(reverse("message"))
(message,) = (item.payload for item in items if item.type == "event")
assert message["message"] == "hi"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
else:
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert not len(transaction["spans"])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_signals_spans(
sentry_init,
client,
capture_events,
capture_items,
render_span_tree,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False),
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "span")
client.get(reverse("message"))
(message,) = (item.payload for item in items if item.type == "event")
assert message["message"] == "hi"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert (
render_span_tree(spans)
== """\
- sentry.op="http.server": name="/message"
- sentry.op="event.django": name="django.db.reset_queries"
- sentry.op="event.django": name="django.db.close_old_connections"\
"""
)
assert spans[0]["attributes"]["sentry.op"] == "event.django"
assert spans[0]["name"] == "django.db.reset_queries"
assert spans[1]["attributes"]["sentry.op"] == "event.django"
assert spans[1]["name"] == "django.db.close_old_connections"
else:
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert (
render_span_tree(transaction["spans"], transaction["contexts"]["trace"])
== """\
- op="http.server": description=null
- op="event.django": description="django.db.reset_queries"
- op="event.django": description="django.db.close_old_connections"\
"""
)
assert transaction["spans"][0]["op"] == "event.django"
assert transaction["spans"][0]["description"] == "django.db.reset_queries"
assert transaction["spans"][1]["op"] == "event.django"
assert (
transaction["spans"][1]["description"] == "django.db.close_old_connections"
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_signals_spans_disabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False, signals_spans=False),
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "span")
client.get(reverse("message"))
sentry_sdk.flush()
(message,) = (item.payload for item in items if item.type == "event")
assert message["message"] == "hi"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
assert len(spans) == 1
else:
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert not transaction["spans"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_signals_spans_filtering(
sentry_init,
client,
capture_events,
capture_items,
render_span_tree,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=False,
signals_denylist=[
myapp_custom_signal_silenced,
],
),
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("send_myapp_custom_signal"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert (
render_span_tree(spans)
== """\
- sentry.op="http.server": name="/send-myapp-custom-signal"
- sentry.op="event.django": name="django.db.reset_queries"
- sentry.op="event.django": name="django.db.close_old_connections"
- sentry.op="event.django": name="tests.integrations.django.myapp.signals.signal_handler"\
"""
)
assert spans[0]["attributes"]["sentry.op"] == "event.django"
assert spans[0]["name"] == "django.db.reset_queries"
assert spans[1]["attributes"]["sentry.op"] == "event.django"
assert spans[1]["name"] == "django.db.close_old_connections"
assert spans[2]["attributes"]["sentry.op"] == "event.django"
assert (
spans[2]["name"] == "tests.integrations.django.myapp.signals.signal_handler"
)
else:
events = capture_events()
client.get(reverse("send_myapp_custom_signal"))
(transaction,) = events
assert (
render_span_tree(transaction["spans"], transaction["contexts"]["trace"])
== """\
- op="http.server": description=null
- op="event.django": description="django.db.reset_queries"
- op="event.django": description="django.db.close_old_connections"
- op="event.django": description="tests.integrations.django.myapp.signals.signal_handler"\
"""
)
assert transaction["spans"][0]["op"] == "event.django"
assert transaction["spans"][0]["description"] == "django.db.reset_queries"
assert transaction["spans"][1]["op"] == "event.django"
assert (
transaction["spans"][1]["description"] == "django.db.close_old_connections"
)
assert transaction["spans"][2]["op"] == "event.django"
assert (
transaction["spans"][2]["description"]
== "tests.integrations.django.myapp.signals.signal_handler"
)
def test_csrf(sentry_init, client):
"""
Assert that CSRF view decorator works even with the view wrapped in our own
callable.
"""
sentry_init(integrations=[DjangoIntegration()])
content, status, _headers = unpack_werkzeug_response(
client.post(reverse("csrf_hello_not_exempt"))
)
assert status.lower() == "403 forbidden"
content, status, _headers = unpack_werkzeug_response(
client.post(reverse("sentryclass_csrf"))
)
assert status.lower() == "403 forbidden"
content, status, _headers = unpack_werkzeug_response(
client.post(reverse("sentryclass"))
)
assert status.lower() == "200 ok"
assert content == b"ok"
content, status, _headers = unpack_werkzeug_response(
client.post(reverse("classbased"))
)
assert status.lower() == "200 ok"
assert content == b"ok"
content, status, _headers = unpack_werkzeug_response(
client.post(reverse("message"))
)
assert status.lower() == "200 ok"
assert content == b"ok"
@pytest.mark.skipif(DJANGO_VERSION < (2, 0), reason="Requires Django > 2.0")
@pytest.mark.parametrize("middleware_spans", [False, True])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_custom_urlconf_middleware(
settings,
sentry_init,
client,
capture_events,
capture_items,
render_span_tree,
middleware_spans,
span_streaming,
):
"""
Some middlewares (for instance in django-tenants) overwrite request.urlconf.
Test that the resolver picks up the correct urlconf for transaction naming.
"""
urlconf = "tests.integrations.django.myapp.middleware.custom_urlconf_middleware"
settings.ROOT_URLCONF = ""
settings.MIDDLEWARE.insert(0, urlconf)
client.application.load_middleware()
sentry_init(
integrations=[DjangoIntegration(middleware_spans=middleware_spans)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("event", "span")
try:
content, status, _headers = unpack_werkzeug_response(
client.get("/custom/ok")
)
assert status.lower() == "200 ok"
assert content == b"custom ok"
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
if middleware_spans:
assert spans[10]["name"] == "/custom/ok"
assert "custom_urlconf_middleware" in render_span_tree(spans)
else:
assert spans[2]["name"] == "/custom/ok"
_content, status, _headers = unpack_werkzeug_response(
client.get("/custom/exc")
)
assert status.lower() == "500 internal server error"
(error_event,) = (item.payload for item in items if item.type == "event")
assert error_event["transaction"] == "/custom/exc"
assert (
error_event["exception"]["values"][-1]["mechanism"]["type"] == "django"
)
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
if middleware_spans:
assert spans[22]["name"] == "/custom/exc"
assert "custom_urlconf_middleware" in render_span_tree(spans)
else:
assert spans[6]["name"] == "/custom/exc"
finally:
settings.MIDDLEWARE.pop(0)
client.application.load_middleware()
else:
events = capture_events()
try:
content, status, _headers = unpack_werkzeug_response(
client.get("/custom/ok")
)
assert status.lower() == "200 ok"
assert content == b"custom ok"
event = events.pop(0)
assert event["transaction"] == "/custom/ok"
if middleware_spans:
assert "custom_urlconf_middleware" in render_span_tree(
event["spans"], event["contexts"]["trace"]
)
_content, status, _headers = unpack_werkzeug_response(
client.get("/custom/exc")
)
assert status.lower() == "500 internal server error"
error_event, transaction_event = events
assert error_event["transaction"] == "/custom/exc"
assert (
error_event["exception"]["values"][-1]["mechanism"]["type"] == "django"
)
assert transaction_event["transaction"] == "/custom/exc"
if middleware_spans:
assert "custom_urlconf_middleware" in render_span_tree(
transaction_event["spans"], transaction_event["contexts"]["trace"]
)
finally:
settings.MIDDLEWARE.pop(0)
client.application.load_middleware()
def test_get_receiver_name():
def dummy(a, b):
return a + b
name = _get_receiver_name(dummy)
assert (
name
== "tests.integrations.django.test_basic.test_get_receiver_name..dummy"
)
a_partial = partial(dummy)
name = _get_receiver_name(a_partial)
if PY310:
assert name == "functools.partial()"
else:
assert name == "partial()"
@pytest.mark.skipif(DJANGO_VERSION <= (1, 11), reason="Requires Django > 1.11")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=True,
signals_spans=True,
cache_spans=True,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
signal_span_found = False
if span_streaming:
items = capture_items("span")
client.get(reverse("view_with_signal"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[-1]["attributes"]["sentry.origin"] == "auto.http.django"
for span in spans:
assert span["attributes"]["sentry.origin"] == "auto.http.django"
if span["attributes"]["sentry.op"] == "event.django":
signal_span_found = True
else:
events = capture_events()
client.get(reverse("view_with_signal"))
(transaction,) = events
assert transaction["contexts"]["trace"]["origin"] == "auto.http.django"
for span in transaction["spans"]:
assert span["origin"] == "auto.http.django"
if span["op"] == "event.django":
signal_span_found = True
assert signal_span_found
@pytest.mark.parametrize("span_streaming", [True, False])
def test_transaction_http_method_default(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[2]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "GET"
else:
events = capture_events()
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
(event,) = events
assert len(events) == 1
assert event["request"]["method"] == "GET"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_transaction_http_method_custom(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[4]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "OPTIONS"
assert spans[7]["attributes"][SPANDATA.HTTP_REQUEST_METHOD] == "HEAD"
else:
events = capture_events()
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
assert len(events) == 2
(event1, event2) = events
assert event1["request"]["method"] == "OPTIONS"
assert event2["request"]["method"] == "HEAD"
def test_ensures_spotlight_middleware_when_spotlight_is_enabled(sentry_init, settings):
"""
Test that ensures if Spotlight is enabled, relevant SpotlightMiddleware
is added to middleware list in settings.
"""
settings.DEBUG = True
original_middleware = frozenset(settings.MIDDLEWARE)
sentry_init(integrations=[DjangoIntegration()], spotlight=True)
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
assert "sentry_sdk.spotlight.SpotlightMiddleware" in added
def test_ensures_no_spotlight_middleware_when_env_killswitch_is_false(
monkeypatch, sentry_init, settings
):
"""
Test that ensures if Spotlight is enabled, but is set to a falsy value
the relevant SpotlightMiddleware is NOT added to middleware list in settings.
"""
settings.DEBUG = True
monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "no")
original_middleware = frozenset(settings.MIDDLEWARE)
sentry_init(integrations=[DjangoIntegration()], spotlight=True)
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added
def test_ensures_no_spotlight_middleware_when_no_spotlight(
monkeypatch, sentry_init, settings
):
"""
Test that ensures if Spotlight is not enabled
the relevant SpotlightMiddleware is NOT added to middleware list in settings.
"""
settings.DEBUG = True
# We should NOT have the middleware even if the env var is truthy if Spotlight is off
monkeypatch.setenv("SENTRY_SPOTLIGHT_ON_ERROR", "1")
original_middleware = frozenset(settings.MIDDLEWARE)
sentry_init(integrations=[DjangoIntegration()], spotlight=False)
added = frozenset(settings.MIDDLEWARE) ^ original_middleware
assert "sentry_sdk.spotlight.SpotlightMiddleware" not in added
def test_get_frame_name_when_in_lazy_object():
allowed_to_init = False
class SimpleLazyObjectWrapper(SimpleLazyObject):
def unproxied_method(self):
"""
For testing purposes. We inject a method on the SimpleLazyObject
class so if python is executing this method, we should get
this class instead of the wrapped class and avoid evaluating
the wrapped object too early.
"""
return inspect.currentframe()
class GetFrame:
def __init__(self):
assert allowed_to_init, "GetFrame not permitted to initialize yet"
def proxied_method(self):
"""
For testing purposes. We add an proxied method on the instance
class so if python is executing this method, we should get
this class instead of the wrapper class.
"""
return inspect.currentframe()
instance = SimpleLazyObjectWrapper(lambda: GetFrame())
assert get_frame_name(instance.unproxied_method()) == (
"SimpleLazyObjectWrapper.unproxied_method"
if sys.version_info < (3, 11)
else "test_get_frame_name_when_in_lazy_object..SimpleLazyObjectWrapper.unproxied_method"
)
# Now that we're about to access an instance method on the wrapped class,
# we should permit initializing it
allowed_to_init = True
assert get_frame_name(instance.proxied_method()) == (
"GetFrame.proxied_method"
if sys.version_info < (3, 11)
else "test_get_frame_name_when_in_lazy_object..GetFrame.proxied_method"
)
sentry-python-2.64.0/tests/integrations/django/test_cache_module.py 0000664 0000000 0000000 00000117261 15220673775 0025627 0 ustar 00root root 0000000 0000000 import os
import random
import uuid
import pytest
from django import VERSION as DJANGO_VERSION
from werkzeug.test import Client
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.django.caching import _get_span_description
from tests.integrations.django.myapp.wsgi import application
from tests.integrations.django.utils import pytest_mark_django_db_decorator
DJANGO_VERSION = DJANGO_VERSION[:2]
@pytest.fixture
def client():
return Client(application)
@pytest.fixture
def use_django_caching(settings):
settings.CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake-%s" % random.randint(1, 1000000),
}
}
@pytest.fixture
def use_django_caching_with_middlewares(settings):
settings.CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake-%s" % random.randint(1, 1000000),
}
}
if hasattr(settings, "MIDDLEWARE"):
middleware = settings.MIDDLEWARE
elif hasattr(settings, "MIDDLEWARE_CLASSES"):
middleware = settings.MIDDLEWARE_CLASSES
else:
middleware = None
if middleware is not None:
middleware.insert(0, "django.middleware.cache.UpdateCacheMiddleware")
middleware.append("django.middleware.cache.FetchFromCacheMiddleware")
@pytest.fixture
def use_django_caching_with_port(settings):
settings.CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
"LOCATION": "redis://username:password@127.0.0.1:6379",
}
}
@pytest.fixture
def use_django_caching_without_port(settings):
settings.CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
"LOCATION": "redis://example.com",
}
}
@pytest.fixture
def use_django_caching_with_cluster(settings):
settings.CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
"LOCATION": [
"redis://127.0.0.1:6379",
"redis://127.0.0.2:6378",
"redis://127.0.0.3:6377",
],
}
}
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_disabled_middleware(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching_with_middlewares,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("not_cached_view"))
client.get(reverse("not_cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 2
else:
events = capture_events()
client.get(reverse("not_cached_view"))
client.get(reverse("not_cached_view"))
(first_event, second_event) = events
assert len(first_event["spans"]) == 0
assert len(second_event["spans"]) == 0
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_disabled_decorator(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 2
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
(first_event, second_event) = events
assert len(first_event["spans"]) == 0
assert len(second_event["spans"]) == 0
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_disabled_templatetag(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("view_with_cached_template_fragment"))
client.get(reverse("view_with_cached_template_fragment"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 2
else:
events = capture_events()
client.get(reverse("view_with_cached_template_fragment"))
client.get(reverse("view_with_cached_template_fragment"))
(first_event, second_event) = events
assert len(first_event["spans"]) == 0
assert len(second_event["spans"]) == 0
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_middleware(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching_with_middlewares,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client.application.load_middleware()
if span_streaming:
items = capture_items("span")
client.get(reverse("not_cached_view"))
client.get(reverse("not_cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# first_event - cache.get
assert spans[0]["attributes"]["sentry.op"] == "cache.get"
assert spans[0]["name"].startswith("views.decorators.cache.cache_header.")
assert spans[0]["attributes"]["network.peer.address"] is not None
assert spans[0]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert not spans[0]["attributes"]["cache.hit"]
assert "cache.item_size" not in spans[0]["attributes"]
# first_event - cache.put
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert spans[1]["name"].startswith("views.decorators.cache.cache_header.")
assert spans[1]["attributes"]["network.peer.address"] is not None
assert spans[1]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert "cache.hit" not in spans[1]["attributes"]
assert spans[1]["attributes"]["cache.item_size"] == 2
# second_event - cache.get
assert spans[4]["attributes"]["sentry.op"] == "cache.get"
assert spans[4]["name"].startswith("views.decorators.cache.cache_header.")
assert spans[4]["attributes"]["network.peer.address"] is not None
assert spans[4]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert spans[4]["attributes"]["cache.hit"]
assert spans[4]["attributes"]["cache.item_size"] == 2
# second_event - cache.get 2
assert spans[5]["attributes"]["sentry.op"] == "cache.get"
assert spans[5]["name"].startswith("views.decorators.cache.cache_page.")
assert spans[5]["attributes"]["network.peer.address"] is not None
assert spans[5]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_page."
)
assert spans[5]["attributes"]["cache.hit"]
assert spans[5]["attributes"]["cache.item_size"] == 58
else:
events = capture_events()
client.get(reverse("not_cached_view"))
client.get(reverse("not_cached_view"))
(first_event, second_event) = events
# first_event - cache.get
assert first_event["spans"][0]["op"] == "cache.get"
assert first_event["spans"][0]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert first_event["spans"][0]["data"]["network.peer.address"] is not None
assert first_event["spans"][0]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert not first_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in first_event["spans"][0]["data"]
# first_event - cache.put
assert first_event["spans"][1]["op"] == "cache.put"
assert first_event["spans"][1]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert first_event["spans"][1]["data"]["network.peer.address"] is not None
assert first_event["spans"][1]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert "cache.hit" not in first_event["spans"][1]["data"]
assert first_event["spans"][1]["data"]["cache.item_size"] == 2
# second_event - cache.get
assert second_event["spans"][0]["op"] == "cache.get"
assert second_event["spans"][0]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert second_event["spans"][0]["data"]["network.peer.address"] is not None
assert second_event["spans"][0]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
# second_event - cache.get 2
assert second_event["spans"][1]["op"] == "cache.get"
assert second_event["spans"][1]["description"].startswith(
"views.decorators.cache.cache_page."
)
assert second_event["spans"][1]["data"]["network.peer.address"] is not None
assert second_event["spans"][1]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_page."
)
assert second_event["spans"][1]["data"]["cache.hit"]
assert second_event["spans"][1]["data"]["cache.item_size"] == 58
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_decorator(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# first_event - cache.get
assert spans[0]["attributes"]["sentry.op"] == "cache.get"
assert spans[0]["name"].startswith("views.decorators.cache.cache_header.")
assert spans[0]["attributes"]["network.peer.address"] is not None
assert spans[0]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert not spans[0]["attributes"]["cache.hit"]
assert "cache.item_size" not in spans[0]["attributes"]
# first_event - cache.put
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert spans[1]["name"].startswith("views.decorators.cache.cache_header.")
assert spans[1]["attributes"]["network.peer.address"] is not None
assert spans[1]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert "cache.hit" not in spans[1]["attributes"]
assert spans[1]["attributes"]["cache.item_size"] == 2
# second_event - cache.get
assert spans[5]["attributes"]["sentry.op"] == "cache.get"
assert spans[5]["name"].startswith("views.decorators.cache.cache_page.")
assert spans[5]["attributes"]["network.peer.address"] is not None
assert spans[5]["attributes"]["cache.key"][0].startswith(
"views.decorators.cache.cache_page."
)
assert spans[5]["attributes"]["cache.hit"]
assert spans[5]["attributes"]["cache.item_size"] == 58
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
(first_event, second_event) = events
# first_event - cache.get
assert first_event["spans"][0]["op"] == "cache.get"
assert first_event["spans"][0]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert first_event["spans"][0]["data"]["network.peer.address"] is not None
assert first_event["spans"][0]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert not first_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in first_event["spans"][0]["data"]
# first_event - cache.put
assert first_event["spans"][1]["op"] == "cache.put"
assert first_event["spans"][1]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert first_event["spans"][1]["data"]["network.peer.address"] is not None
assert first_event["spans"][1]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert "cache.hit" not in first_event["spans"][1]["data"]
assert first_event["spans"][1]["data"]["cache.item_size"] == 2
# second_event - cache.get
assert second_event["spans"][1]["op"] == "cache.get"
assert second_event["spans"][1]["description"].startswith(
"views.decorators.cache.cache_page."
)
assert second_event["spans"][1]["data"]["network.peer.address"] is not None
assert second_event["spans"][1]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_page."
)
assert second_event["spans"][1]["data"]["cache.hit"]
assert second_event["spans"][1]["data"]["cache.item_size"] == 58
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION < (1, 9), reason="Requires Django >= 1.9")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_templatetag(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("view_with_cached_template_fragment"))
client.get(reverse("view_with_cached_template_fragment"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 5
# first_event - cache.get
assert spans[0]["attributes"]["sentry.op"] == "cache.get"
assert spans[0]["name"].startswith("template.cache.some_identifier.")
assert spans[0]["attributes"]["network.peer.address"] is not None
assert spans[0]["attributes"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert not spans[0]["attributes"]["cache.hit"]
assert "cache.item_size" not in spans[0]["attributes"]
# first_event - cache.put
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert spans[1]["name"].startswith("template.cache.some_identifier.")
assert spans[1]["attributes"]["network.peer.address"] is not None
assert spans[1]["attributes"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert "cache.hit" not in spans[1]["attributes"]
assert spans[1]["attributes"]["cache.item_size"] == 51
# second_event - cache.get
assert spans[3]["attributes"]["sentry.op"] == "cache.get"
assert spans[3]["name"].startswith("template.cache.some_identifier.")
assert spans[3]["attributes"]["network.peer.address"] is not None
assert spans[3]["attributes"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert spans[3]["attributes"]["cache.hit"]
assert spans[3]["attributes"]["cache.item_size"] == 51
else:
events = capture_events()
client.get(reverse("view_with_cached_template_fragment"))
client.get(reverse("view_with_cached_template_fragment"))
(first_event, second_event) = events
assert len(first_event["spans"]) == 2
# first_event - cache.get
assert first_event["spans"][0]["op"] == "cache.get"
assert first_event["spans"][0]["description"].startswith(
"template.cache.some_identifier."
)
assert first_event["spans"][0]["data"]["network.peer.address"] is not None
assert first_event["spans"][0]["data"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert not first_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in first_event["spans"][0]["data"]
# first_event - cache.put
assert first_event["spans"][1]["op"] == "cache.put"
assert first_event["spans"][1]["description"].startswith(
"template.cache.some_identifier."
)
assert first_event["spans"][1]["data"]["network.peer.address"] is not None
assert first_event["spans"][1]["data"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert "cache.hit" not in first_event["spans"][1]["data"]
assert first_event["spans"][1]["data"]["cache.item_size"] == 51
# second_event - cache.get
assert second_event["spans"][0]["op"] == "cache.get"
assert second_event["spans"][0]["description"].startswith(
"template.cache.some_identifier."
)
assert second_event["spans"][0]["data"]["network.peer.address"] is not None
assert second_event["spans"][0]["data"]["cache.key"][0].startswith(
"template.cache.some_identifier."
)
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 51
@pytest.mark.parametrize(
"method_name, args, kwargs, expected_name",
[
(None, None, None, ""),
("get", None, None, ""),
("get", [], {}, ""),
("get", ["bla", "blub", "foo"], {}, "bla"),
("get", [uuid.uuid4().bytes], {}, ""),
(
"get_many",
[["bla1", "bla2", "bla3"], "blub", "foo"],
{},
"bla1, bla2, bla3",
),
(
"get_many",
[["bla:1", "bla:2", "bla:3"], "blub", "foo"],
{"key": "bar"},
"bla:1, bla:2, bla:3",
),
("get", [], {"key": "bar"}, "bar"),
(
"get",
"something",
{},
"s",
), # this case should never happen, just making sure that we are not raising an exception in that case.
],
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_get_span_name(
method_name, args, kwargs, expected_name, span_streaming
):
assert _get_span_description(method_name, args, kwargs) == expected_name
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_location_with_port(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching_with_port,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["is_segment"] is True:
continue
assert (
span["attributes"]["network.peer.address"] == "redis://127.0.0.1"
) # Note: the username/password are not included in the address
assert span["attributes"]["network.peer.port"] == 6379
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
for event in events:
for span in event["spans"]:
assert (
span["data"]["network.peer.address"] == "redis://127.0.0.1"
) # Note: the username/password are not included in the address
assert span["data"]["network.peer.port"] == 6379
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_location_without_port(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching_without_port,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["is_segment"] is True:
continue
assert span["attributes"]["network.peer.address"] == "redis://example.com"
assert "network.peer.port" not in span["attributes"]
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
for event in events:
for span in event["spans"]:
assert span["data"]["network.peer.address"] == "redis://example.com"
assert "network.peer.port" not in span["data"]
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_location_with_cluster(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching_with_cluster,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
# because it is a cluster we do not know what host is actually accessed, so we omit the data
assert "network.peer.address" not in span["attributes"].keys()
assert "network.peer.port" not in span["attributes"].keys()
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
for event in events:
for span in event["spans"]:
# because it is a cluster we do not know what host is actually accessed, so we omit the data
assert "network.peer.address" not in span["data"].keys()
assert "network.peer.port" not in span["data"].keys()
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_item_size(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 7
assert spans[0]["attributes"]["sentry.op"] == "cache.get"
assert not spans[0]["attributes"]["cache.hit"]
assert "cache.item_size" not in spans[0]["attributes"]
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert "cache.hit" not in spans[1]["attributes"]
assert spans[1]["attributes"]["cache.item_size"] == 2
assert spans[2]["attributes"]["sentry.op"] == "cache.put"
assert "cache.hit" not in spans[2]["attributes"]
assert spans[2]["attributes"]["cache.item_size"] == 58
assert spans[4]["attributes"]["sentry.op"] == "cache.get"
assert spans[4]["attributes"]["cache.hit"]
assert spans[4]["attributes"]["cache.item_size"] == 2
assert spans[5]["attributes"]["sentry.op"] == "cache.get"
assert spans[5]["attributes"]["cache.hit"]
assert spans[5]["attributes"]["cache.item_size"] == 58
else:
events = capture_events()
client.get(reverse("cached_view"))
client.get(reverse("cached_view"))
(first_event, second_event) = events
assert len(first_event["spans"]) == 3
assert first_event["spans"][0]["op"] == "cache.get"
assert not first_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in first_event["spans"][0]["data"]
assert first_event["spans"][1]["op"] == "cache.put"
assert "cache.hit" not in first_event["spans"][1]["data"]
assert first_event["spans"][1]["data"]["cache.item_size"] == 2
assert first_event["spans"][2]["op"] == "cache.put"
assert "cache.hit" not in first_event["spans"][2]["data"]
assert first_event["spans"][2]["data"]["cache.item_size"] == 58
assert len(second_event["spans"]) == 2
assert second_event["spans"][0]["op"] == "cache.get"
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
assert second_event["spans"][1]["op"] == "cache.get"
assert second_event["spans"][1]["data"]["cache.hit"]
assert second_event["spans"][1]["data"]["cache.item_size"] == 58
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_get_custom_default(
sentry_init,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
id = os.getpid()
from django.core.cache import cache
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
cache.set(f"S{id}", "Sensitive1")
cache.set(f"S{id + 1}", "")
cache.get(f"S{id}", "null")
cache.get(f"S{id}", default="null")
cache.get(f"S{id + 1}", "null")
cache.get(f"S{id + 1}", default="null")
cache.get(f"S{id + 2}", "null")
cache.get(f"S{id + 2}", default="null")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 9
assert spans[0]["attributes"]["sentry.op"] == "cache.put"
assert spans[0]["name"] == f"S{id}"
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert spans[1]["name"] == f"S{id + 1}"
for span in (spans[2], spans[3]):
assert span["attributes"]["sentry.op"] == "cache.get"
assert span["name"] == f"S{id}"
assert span["attributes"]["cache.hit"]
assert span["attributes"]["cache.item_size"] == 10
for span in (spans[4], spans[5]):
assert span["attributes"]["sentry.op"] == "cache.get"
assert span["name"] == f"S{id + 1}"
assert span["attributes"]["cache.hit"]
assert span["attributes"]["cache.item_size"] == 0
for span in (spans[6], spans[7]):
assert span["attributes"]["sentry.op"] == "cache.get"
assert span["name"] == f"S{id + 2}"
assert not span["attributes"]["cache.hit"]
assert "cache.item_size" not in span["attributes"]
else:
events = capture_events()
with sentry_sdk.start_transaction():
cache.set(f"S{id}", "Sensitive1")
cache.set(f"S{id + 1}", "")
cache.get(f"S{id}", "null")
cache.get(f"S{id}", default="null")
cache.get(f"S{id + 1}", "null")
cache.get(f"S{id + 1}", default="null")
cache.get(f"S{id + 2}", "null")
cache.get(f"S{id + 2}", default="null")
(transaction,) = events
assert len(transaction["spans"]) == 8
assert transaction["spans"][0]["op"] == "cache.put"
assert transaction["spans"][0]["description"] == f"S{id}"
assert transaction["spans"][1]["op"] == "cache.put"
assert transaction["spans"][1]["description"] == f"S{id + 1}"
for span in (transaction["spans"][2], transaction["spans"][3]):
assert span["op"] == "cache.get"
assert span["description"] == f"S{id}"
assert span["data"]["cache.hit"]
assert span["data"]["cache.item_size"] == 10
for span in (transaction["spans"][4], transaction["spans"][5]):
assert span["op"] == "cache.get"
assert span["description"] == f"S{id + 1}"
assert span["data"]["cache.hit"]
assert span["data"]["cache.item_size"] == 0
for span in (transaction["spans"][6], transaction["spans"][7]):
assert span["op"] == "cache.get"
assert span["description"] == f"S{id + 2}"
assert not span["data"]["cache.hit"]
assert "cache.item_size" not in span["data"]
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_get_many(
sentry_init,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
id = os.getpid()
from django.core.cache import cache
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
cache.get_many([f"S{id}", f"S{id + 1}"])
cache.set(f"S{id}", "Sensitive1")
cache.get_many([f"S{id}", f"S{id + 1}"])
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 8
assert spans[2]["attributes"]["sentry.op"] == "cache.get"
assert spans[2]["name"] == f"S{id}, S{id + 1}"
assert not spans[2]["attributes"]["cache.hit"]
assert spans[0]["attributes"]["sentry.op"] == "cache.get"
assert spans[0]["name"] == f"S{id}"
assert not spans[0]["attributes"]["cache.hit"]
assert spans[1]["attributes"]["sentry.op"] == "cache.get"
assert spans[1]["name"] == f"S{id + 1}"
assert not spans[1]["attributes"]["cache.hit"]
assert spans[3]["attributes"]["sentry.op"] == "cache.put"
assert spans[3]["name"] == f"S{id}"
assert spans[6]["attributes"]["sentry.op"] == "cache.get"
assert spans[6]["name"] == f"S{id}, S{id + 1}"
assert spans[6]["attributes"]["cache.hit"]
assert spans[4]["attributes"]["sentry.op"] == "cache.get"
assert spans[4]["name"] == f"S{id}"
assert spans[4]["attributes"]["cache.hit"]
assert spans[5]["attributes"]["sentry.op"] == "cache.get"
assert spans[5]["name"] == f"S{id + 1}"
assert not spans[5]["attributes"]["cache.hit"]
else:
events = capture_events()
with sentry_sdk.start_transaction():
cache.get_many([f"S{id}", f"S{id + 1}"])
cache.set(f"S{id}", "Sensitive1")
cache.get_many([f"S{id}", f"S{id + 1}"])
(transaction,) = events
assert len(transaction["spans"]) == 7
assert transaction["spans"][0]["op"] == "cache.get"
assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}"
assert not transaction["spans"][0]["data"]["cache.hit"]
assert transaction["spans"][1]["op"] == "cache.get"
assert transaction["spans"][1]["description"] == f"S{id}"
assert not transaction["spans"][1]["data"]["cache.hit"]
assert transaction["spans"][2]["op"] == "cache.get"
assert transaction["spans"][2]["description"] == f"S{id + 1}"
assert not transaction["spans"][2]["data"]["cache.hit"]
assert transaction["spans"][3]["op"] == "cache.put"
assert transaction["spans"][3]["description"] == f"S{id}"
assert transaction["spans"][4]["op"] == "cache.get"
assert transaction["spans"][4]["description"] == f"S{id}, S{id + 1}"
assert transaction["spans"][4]["data"]["cache.hit"]
assert transaction["spans"][5]["op"] == "cache.get"
assert transaction["spans"][5]["description"] == f"S{id}"
assert transaction["spans"][5]["data"]["cache.hit"]
assert transaction["spans"][6]["op"] == "cache.get"
assert transaction["spans"][6]["description"] == f"S{id + 1}"
assert not transaction["spans"][6]["data"]["cache.hit"]
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_cache_spans_set_many(
sentry_init,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
id = os.getpid()
from django.core.cache import cache
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
cache.set_many({f"S{id}": "Sensitive1", f"S{id + 1}": "Sensitive2"})
cache.get(f"S{id}")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert len(spans) == 5
assert spans[2]["attributes"]["sentry.op"] == "cache.put"
assert spans[2]["name"] == f"S{id}, S{id + 1}"
assert spans[0]["attributes"]["sentry.op"] == "cache.put"
assert spans[0]["name"] == f"S{id}"
assert spans[1]["attributes"]["sentry.op"] == "cache.put"
assert spans[1]["name"] == f"S{id + 1}"
assert spans[3]["attributes"]["sentry.op"] == "cache.get"
assert spans[3]["name"] == f"S{id}"
else:
events = capture_events()
with sentry_sdk.start_transaction():
cache.set_many({f"S{id}": "Sensitive1", f"S{id + 1}": "Sensitive2"})
cache.get(f"S{id}")
(transaction,) = events
assert len(transaction["spans"]) == 4
assert transaction["spans"][0]["op"] == "cache.put"
assert transaction["spans"][0]["description"] == f"S{id}, S{id + 1}"
assert transaction["spans"][1]["op"] == "cache.put"
assert transaction["spans"][1]["description"] == f"S{id}"
assert transaction["spans"][2]["op"] == "cache.put"
assert transaction["spans"][2]["description"] == f"S{id + 1}"
assert transaction["spans"][3]["op"] == "cache.get"
assert transaction["spans"][3]["description"] == f"S{id}"
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.skipif(DJANGO_VERSION <= (1, 11), reason="Requires Django > 1.11")
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin_cache(
sentry_init,
client,
capture_events,
capture_items,
use_django_caching,
span_streaming,
):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=True,
signals_spans=True,
cache_spans=True,
)
],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
cache_span_found = False
if span_streaming:
items = capture_items("span")
client.get(reverse("cached_view"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[1]["attributes"]["sentry.origin"] == "auto.http.django"
for span in spans:
assert span["attributes"]["sentry.origin"] == "auto.http.django"
if span["attributes"]["sentry.op"].startswith("cache."):
cache_span_found = True
else:
events = capture_events()
client.get(reverse("cached_view"))
(transaction,) = events
assert transaction["contexts"]["trace"]["origin"] == "auto.http.django"
for span in transaction["spans"]:
assert span["origin"] == "auto.http.django"
if span["op"].startswith("cache."):
cache_span_found = True
assert cache_span_found
sentry-python-2.64.0/tests/integrations/django/test_data_scrubbing.py 0000664 0000000 0000000 00000006124 15220673775 0026161 0 ustar 00root root 0000000 0000000 import pytest
from werkzeug.test import Client
from sentry_sdk.integrations.django import DjangoIntegration
from tests.conftest import werkzeug_set_cookie
from tests.integrations.django.myapp.wsgi import application
from tests.integrations.django.utils import pytest_mark_django_db_decorator
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
@pytest.fixture
def client():
return Client(application)
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_scrub_django_session_cookies_removed(
sentry_init,
client,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=False,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
items = capture_items("event")
werkzeug_set_cookie(client, "localhost", "sessionid", "123")
werkzeug_set_cookie(client, "localhost", "csrftoken", "456")
werkzeug_set_cookie(client, "localhost", "foo", "bar")
client.get(reverse("view_exc"))
(event,) = (item.payload for item in items if item.type == "event")
assert "cookies" not in event["request"]
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_scrub_django_session_cookies_filtered(
sentry_init,
client,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
items = capture_items("event")
werkzeug_set_cookie(client, "localhost", "sessionid", "123")
werkzeug_set_cookie(client, "localhost", "csrftoken", "456")
werkzeug_set_cookie(client, "localhost", "foo", "bar")
client.get(reverse("view_exc"))
(event,) = (item.payload for item in items if item.type == "event")
assert event["request"]["cookies"] == {
"sessionid": "[Filtered]",
"csrftoken": "[Filtered]",
"foo": "bar",
}
@pytest.mark.forked
@pytest_mark_django_db_decorator()
@pytest.mark.parametrize("span_streaming", [True, False])
def test_scrub_django_custom_session_cookies_filtered(
sentry_init,
client,
capture_items,
settings,
span_streaming,
):
settings.SESSION_COOKIE_NAME = "my_sess"
settings.CSRF_COOKIE_NAME = "csrf_secret"
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
items = capture_items("event")
werkzeug_set_cookie(client, "localhost", "my_sess", "123")
werkzeug_set_cookie(client, "localhost", "csrf_secret", "456")
werkzeug_set_cookie(client, "localhost", "foo", "bar")
client.get(reverse("view_exc"))
(event,) = (item.payload for item in items if item.type == "event")
assert event["request"]["cookies"] == {
"my_sess": "[Filtered]",
"csrf_secret": "[Filtered]",
"foo": "bar",
}
sentry-python-2.64.0/tests/integrations/django/test_db_query_data.py 0000664 0000000 0000000 00000100636 15220673775 0026020 0 ustar 00root root 0000000 0000000 import os
from datetime import datetime
from unittest import mock
import pytest
from django import VERSION as DJANGO_VERSION
from django.db import connections
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from werkzeug.test import Client
import sentry_sdk
from sentry_sdk import start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.tracing_utils import (
record_sql_queries,
)
from tests.conftest import unpack_werkzeug_response
from tests.integrations.django.myapp.wsgi import application
from tests.integrations.django.utils import pytest_mark_django_db_decorator
@pytest.fixture
def client():
return Client(application)
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_disabled(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_options = {
"integrations": [DjangoIntegration()],
"send_default_pii": True,
"traces_sample_rate": 1.0,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
"_experiments": {"trace_lifecycle": "stream" if span_streaming else "static"},
}
sentry_init(**sentry_options)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER not in attributes
assert SPANDATA.CODE_NAMESPACE not in attributes
assert SPANDATA.CODE_FILE_PATH not in attributes
assert SPANDATA.CODE_FUNCTION not in attributes
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("enable_db_query_source", [None, True])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_enabled(
sentry_init,
client,
capture_events,
capture_items,
enable_db_query_source,
span_streaming,
):
sentry_options = {
"integrations": [DjangoIntegration()],
"send_default_pii": True,
"traces_sample_rate": 1.0,
"db_query_source_threshold_ms": 0,
"_experiments": {"trace_lifecycle": "stream" if span_streaming else "static"},
}
if enable_db_query_source is not None:
sentry_options["enable_db_query_source"] = enable_db_query_source
sentry_init(**sentry_options)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int
assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0
assert (
attributes.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.views"
)
assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith(
"tests/integrations/django/myapp/views.py"
)
is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep
assert is_relative_path
assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.views"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/django/myapp/views.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_with_module_in_search_path(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
"""
Test that query source is relative to the path of the module it ran in
"""
client = Client(application)
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_slow_from_supplement"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int
assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0
assert attributes.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views"
assert (
attributes.get(SPANDATA.CODE_FILE_PATH) == "django_helpers/views.py"
)
is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep
assert is_relative_path
assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_slow_from_supplement"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert data.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views"
assert data.get(SPANDATA.CODE_FILEPATH) == "django_helpers/views.py"
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_with_in_app_exclude(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
in_app_exclude=["tests.integrations.django.myapp.views"],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int
assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0
if DJANGO_VERSION >= (1, 11):
assert (
attributes.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.settings"
)
assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith(
"tests/integrations/django/myapp/settings.py"
)
assert attributes.get(SPANDATA.CODE_FUNCTION) == "middleware"
else:
assert (
attributes.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.test_db_query_data"
)
assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith(
"tests/integrations/django/test_db_query_data.py"
)
assert (
attributes.get(SPANDATA.CODE_FUNCTION)
== "test_query_source_with_in_app_exclude"
)
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
if DJANGO_VERSION >= (1, 11):
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.settings"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/django/myapp/settings.py"
)
assert data.get(SPANDATA.CODE_FUNCTION) == "middleware"
else:
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.test_db_query_data"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/django/test_db_query_data.py"
)
assert (
data.get(SPANDATA.CODE_FUNCTION)
== "test_query_source_with_in_app_exclude"
)
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_with_in_app_include(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
in_app_include=["django"],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int
assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0
assert (
attributes.get(SPANDATA.CODE_NAMESPACE)
== "django.db.models.sql.compiler"
)
assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith(
"django/db/models/sql/compiler.py"
)
assert attributes.get(SPANDATA.CODE_FUNCTION) == "execute_sql"
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE) == "django.db.models.sql.compiler"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"django/db/models/sql/compiler.py"
)
assert data.get(SPANDATA.CODE_FUNCTION) == "execute_sql"
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_no_query_source_if_duration_too_short(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
self._ctx_mgr = record_sql_queries(*args, **kwargs)
def __enter__(self):
self.span = self._ctx_mgr.__enter__()
if span_streaming:
self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span._end_timestamp = datetime(2024, 1, 1, microsecond=99999)
return self.span
def __exit__(self, type, value, traceback):
if span_streaming:
self.span._end_timestamp = None
self._ctx_mgr.__exit__(type, value, traceback)
self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span._end_timestamp = datetime(2024, 1, 1, microsecond=99999)
return
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=99999)
self._ctx_mgr.__exit__(type, value, traceback)
if span_streaming:
items = capture_items("span")
with mock.patch(
"sentry_sdk.integrations.django.record_sql_queries",
fake_record_sql_queries,
):
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER not in attributes
assert SPANDATA.CODE_NAMESPACE not in attributes
assert SPANDATA.CODE_FILE_PATH not in attributes
assert SPANDATA.CODE_FUNCTION not in attributes
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
with mock.patch(
"sentry_sdk.integrations.django.record_sql_queries",
fake_record_sql_queries,
):
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO not in data
assert SPANDATA.CODE_NAMESPACE not in data
assert SPANDATA.CODE_FILEPATH not in data
assert SPANDATA.CODE_FUNCTION not in data
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_query_source_if_duration_over_threshold(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
self._ctx_mgr = record_sql_queries(*args, **kwargs)
def __enter__(self):
self.span = self._ctx_mgr.__enter__()
if span_streaming:
self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span._end_timestamp = datetime(2024, 1, 1, microsecond=101000)
return self.span
def __exit__(self, type, value, traceback):
if span_streaming:
self.span._end_timestamp = None
self._ctx_mgr.__exit__(type, value, traceback)
self.span._start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span._end_timestamp = datetime(2024, 1, 1, microsecond=101000)
return
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=101000)
self._ctx_mgr.__exit__(type, value, traceback)
if span_streaming:
items = capture_items("span")
with mock.patch(
"sentry_sdk.integrations.django.record_sql_queries",
fake_record_sql_queries,
):
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
sentry_sdk.flush()
spans = [item.payload for item in items]
for span in spans:
if span["attributes"].get("sentry.op") == "db" and "auth_user" in span.get(
"name"
):
attributes = span.get("attributes", {})
assert SPANDATA.CODE_LINE_NUMBER in attributes
assert SPANDATA.CODE_NAMESPACE in attributes
assert SPANDATA.CODE_FILE_PATH in attributes
assert SPANDATA.CODE_FUNCTION in attributes
assert type(attributes.get(SPANDATA.CODE_LINE_NUMBER)) == int
assert attributes.get(SPANDATA.CODE_LINE_NUMBER) > 0
assert (
attributes.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.views"
)
assert attributes.get(SPANDATA.CODE_FILE_PATH).endswith(
"tests/integrations/django/myapp/views.py"
)
is_relative_path = attributes.get(SPANDATA.CODE_FILE_PATH)[0] != os.sep
assert is_relative_path
assert attributes.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
else:
events = capture_events()
with mock.patch(
"sentry_sdk.integrations.django.record_sql_queries",
fake_record_sql_queries,
):
_, status, _ = unpack_werkzeug_response(
client.get(reverse("postgres_select_orm"))
)
assert status == "200 OK"
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and "auth_user" in span.get("description"):
data = span.get("data", {})
assert SPANDATA.CODE_LINENO in data
assert SPANDATA.CODE_NAMESPACE in data
assert SPANDATA.CODE_FILEPATH in data
assert SPANDATA.CODE_FUNCTION in data
assert type(data.get(SPANDATA.CODE_LINENO)) == int
assert data.get(SPANDATA.CODE_LINENO) > 0
assert (
data.get(SPANDATA.CODE_NAMESPACE)
== "tests.integrations.django.myapp.views"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/django/myapp/views.py"
)
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
break
else:
raise AssertionError("No db span found")
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_span_origin_execute(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_select_orm"))
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[1]["attributes"]["sentry.origin"] == "auto.http.django"
for span in spans:
if span["attributes"]["sentry.op"] == "db":
assert span["attributes"]["sentry.origin"] == "auto.db.django"
else:
assert span["attributes"]["sentry.origin"] == "auto.http.django"
else:
events = capture_events()
client.get(reverse("postgres_select_orm"))
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
for span in event["spans"]:
if span["op"] == "db":
assert span["origin"] == "auto.db.django"
else:
assert span["origin"] == "auto.http.django"
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_span_origin_executemany(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """UPDATE auth_user SET username = %s where id = %s;"""
query_list = (
(
"test1",
1,
),
(
"test2",
2,
),
)
cursor.executemany(query, query_list)
transaction.commit()
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[1]["attributes"]["sentry.origin"] == "manual"
assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django"
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """UPDATE auth_user SET username = %s where id = %s;"""
query_list = (
(
"test1",
1,
),
(
"test2",
2,
),
)
cursor.executemany(query, query_list)
transaction.commit()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.django"
sentry-python-2.64.0/tests/integrations/django/test_db_transactions.py 0000664 0000000 0000000 00000165476 15220673775 0026407 0 ustar 00root root 0000000 0000000 import itertools
import os
from datetime import datetime
import pytest
from django.contrib.auth.models import User
from django.db import connections
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from werkzeug.test import Client
import sentry_sdk
from sentry_sdk import start_transaction
from sentry_sdk.consts import SPANDATA, SPANNAME
from sentry_sdk.integrations.django import DjangoIntegration
from tests.integrations.django.myapp.wsgi import application
from tests.integrations.django.utils import pytest_mark_django_db_decorator
@pytest.fixture
def client():
return Client(application)
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_transaction_spans_disabled_no_autocommit(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_no_autocommit_rollback"))
client.get(reverse("postgres_insert_orm_no_autocommit"))
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.rollback()
transaction.set_autocommit(True)
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.commit()
transaction.set_autocommit(True)
sentry_sdk.flush()
spans = [item.payload for item in items]
postgres_rollback = spans[4]
assert postgres_rollback["is_segment"] is True
postgres_commit = spans[9]
assert postgres_commit["is_segment"] is True
sqlite_rollback = spans[11]
assert sqlite_rollback["is_segment"] is True
sqlite_commit = spans[13]
assert sqlite_commit["is_segment"] is True
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert postgres_rollback["attributes"]["sentry.origin"] == "auto.http.django"
assert postgres_commit["attributes"]["sentry.origin"] == "auto.http.django"
assert sqlite_rollback["attributes"]["sentry.origin"] == "manual"
assert sqlite_commit["attributes"]["sentry.origin"] == "manual"
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
or span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(commit_spans) == 0
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_no_autocommit_rollback"))
client.get(reverse("postgres_insert_orm_no_autocommit"))
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.rollback()
transaction.set_autocommit(True)
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.commit()
transaction.set_autocommit(True)
(postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert postgres_rollback["contexts"]["trace"]["origin"] == "auto.http.django"
assert postgres_commit["contexts"]["trace"]["origin"] == "auto.http.django"
assert sqlite_rollback["contexts"]["trace"]["origin"] == "manual"
assert sqlite_commit["contexts"]["trace"]["origin"] == "manual"
commit_spans = [
span
for span in itertools.chain(
postgres_rollback["spans"],
postgres_commit["spans"],
sqlite_rollback["spans"],
sqlite_commit["spans"],
)
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(commit_spans) == 0
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_transaction_spans_disabled_atomic(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_atomic_rollback"))
client.get(reverse("postgres_insert_orm_atomic"))
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
transaction.set_rollback(True)
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
sentry_sdk.flush()
spans = [item.payload for item in items]
postgres_rollback = spans[4]
assert postgres_rollback["is_segment"] is True
postgres_commit = spans[9]
assert postgres_commit["is_segment"] is True
sqlite_rollback = spans[12]
assert sqlite_rollback["is_segment"] is True
sqlite_commit = spans[15]
assert sqlite_commit["is_segment"] is True
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
or span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_atomic_rollback"))
client.get(reverse("postgres_insert_orm_atomic"))
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
transaction.set_rollback(True)
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
(postgres_rollback, postgres_commit, sqlite_rollback, sqlite_commit) = events
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert postgres_rollback["contexts"]["trace"]["origin"] == "auto.http.django"
assert postgres_commit["contexts"]["trace"]["origin"] == "auto.http.django"
assert sqlite_rollback["contexts"]["trace"]["origin"] == "manual"
assert sqlite_commit["contexts"]["trace"]["origin"] == "manual"
commit_spans = [
span
for span in itertools.chain(
postgres_rollback["spans"],
postgres_commit["spans"],
sqlite_rollback["spans"],
sqlite_commit["spans"],
)
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
or span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(commit_spans) == 0
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_no_autocommit_execute(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_no_autocommit"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django"
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert commit_span["attributes"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert commit_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_no_autocommit"))
(event,) = events
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
commit_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert commit_span["data"].get(SPANDATA.DB_NAME) is not None
assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
assert len(insert_spans) == 1
insert_span = insert_spans[0]
# Verify query and commit statements are siblings
assert commit_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_no_autocommit_executemany(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.commit()
transaction.set_autocommit(True)
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is persisted
assert User.objects.exists()
assert spans[2]["attributes"]["sentry.origin"] == "manual"
assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django"
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite"
conn_params = connection.get_connection_params()
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.commit()
transaction.set_autocommit(True)
(event,) = events
# Ensure operation is persisted
assert User.objects.exists()
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.django"
commit_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite"
conn_params = connection.get_connection_params()
assert commit_span["data"].get(SPANDATA.DB_NAME) is not None
assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
# Verify queries and commit statements are siblings
for insert_span in insert_spans:
assert commit_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_no_autocommit_rollback_execute(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_no_autocommit_rollback"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
assert rollback_span["attributes"].get(
SPANDATA.SERVER_ADDRESS
) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost")
assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_no_autocommit_rollback"))
(event,) = events
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
assert len(insert_spans) == 1
insert_span = insert_spans[0]
# Verify query and rollback statements are siblings
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_no_autocommit_rollback_executemany(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.rollback()
transaction.set_autocommit(True)
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.exists()
assert spans[2]["attributes"]["sentry.origin"] == "manual"
assert spans[0]["attributes"]["sentry.origin"] == "auto.db.django"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
transaction.set_autocommit(False)
cursor.executemany(query, query_list)
transaction.rollback()
transaction.set_autocommit(True)
(event,) = events
# Ensure operation is rolled back
assert not User.objects.exists()
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.django"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
# Verify queries and rollback statements are siblings
for insert_span in insert_spans:
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_execute(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_atomic"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django"
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert commit_span["attributes"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert commit_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_atomic"))
(event,) = events
# Ensure operation is persisted
assert User.objects.using("postgres").exists()
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
commit_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert commit_span["data"].get(SPANDATA.DB_NAME) is not None
assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert commit_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert commit_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
assert len(insert_spans) == 1
insert_span = insert_spans[0]
# Verify query and commit statements are siblings
assert commit_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_executemany(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is persisted
assert User.objects.exists()
assert spans[3]["attributes"]["sentry.origin"] == "manual"
commit_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite"
conn_params = connection.get_connection_params()
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert commit_span["attributes"].get(SPANDATA.DB_NAMESPACE) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
(event,) = events
# Ensure operation is persisted
assert User.objects.exists()
assert event["contexts"]["trace"]["origin"] == "manual"
commit_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_COMMIT
]
assert len(commit_spans) == 1
commit_span = commit_spans[0]
assert commit_span["origin"] == "auto.db.django"
# Verify other database attributes
assert commit_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite"
conn_params = connection.get_connection_params()
assert commit_span["data"].get(SPANDATA.DB_NAME) is not None
assert commit_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
# Verify queries and commit statements are siblings
for insert_span in insert_spans:
assert commit_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_rollback_execute(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_atomic_rollback"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
assert rollback_span["attributes"].get(
SPANDATA.SERVER_ADDRESS
) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost")
assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_atomic_rollback"))
(event,) = events
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
assert len(insert_spans) == 1
insert_span = insert_spans[0]
# Verify query and rollback statements are siblings
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_rollback_executemany(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
transaction.set_rollback(True)
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.exists()
assert spans[3]["attributes"]["sentry.origin"] == "manual"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
transaction.set_rollback(True)
(event,) = events
# Ensure operation is rolled back
assert not User.objects.exists()
assert event["contexts"]["trace"]["origin"] == "manual"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
# Verify queries and rollback statements are siblings
for insert_span in insert_spans:
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_execute_exception(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
# trigger Django to open a new connection by marking the existing one as None.
connections["postgres"].connection = None
if span_streaming:
items = capture_items("span")
client.get(reverse("postgres_insert_orm_atomic_exception"))
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert spans[5]["attributes"]["sentry.origin"] == "auto.http.django"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
assert rollback_span["attributes"].get(
SPANDATA.SERVER_ADDRESS
) == os.environ.get("SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost")
assert rollback_span["attributes"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
client.get(reverse("postgres_insert_orm_atomic_exception"))
(event,) = events
# Ensure operation is rolled back
assert not User.objects.using("postgres").exists()
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "postgresql"
conn_params = connections["postgres"].get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
assert rollback_span["data"].get(SPANDATA.SERVER_ADDRESS) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_HOST", "localhost"
)
assert rollback_span["data"].get(SPANDATA.SERVER_PORT) == os.environ.get(
"SENTRY_PYTHON_TEST_POSTGRES_PORT", "5432"
)
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
assert len(insert_spans) == 1
insert_span = insert_spans[0]
# Verify query and rollback statements are siblings
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_db_atomic_executemany_exception(
sentry_init,
client,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[DjangoIntegration(db_transaction_spans=True)],
send_default_pii=True,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
from django.db import connection, transaction
try:
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
1 / 0
except ZeroDivisionError:
pass
sentry_sdk.flush()
spans = [item.payload for item in items]
# Ensure operation is rolled back
assert not User.objects.exists()
assert spans[3]["attributes"]["sentry.origin"] == "manual"
rollback_spans = [
span
for span in spans
if span["attributes"].get(SPANDATA.DB_OPERATION_NAME)
== SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["attributes"]["sentry.origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["attributes"].get(SPANDATA.DB_SYSTEM_NAME) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["attributes"].get(SPANDATA.DB_NAMESPACE) is not None
assert rollback_span["attributes"].get(
SPANDATA.DB_NAMESPACE
) == conn_params.get("database") or conn_params.get("dbname")
insert_spans = [
span for span in spans if span["name"].startswith("INSERT INTO")
]
else:
events = capture_events()
with start_transaction(name="test_transaction"):
from django.db import connection, transaction
try:
with transaction.atomic():
cursor = connection.cursor()
query = """INSERT INTO auth_user (
password,
is_superuser,
username,
first_name,
last_name,
email,
is_staff,
is_active,
date_joined
)
VALUES ('password', false, %s, %s, %s, %s, false, true, %s);"""
query_list = (
(
"user1",
"John",
"Doe",
"user1@example.com",
datetime(1970, 1, 1),
),
(
"user2",
"Max",
"Mustermann",
"user2@example.com",
datetime(1970, 1, 1),
),
)
cursor.executemany(query, query_list)
1 / 0
except ZeroDivisionError:
pass
(event,) = events
# Ensure operation is rolled back
assert not User.objects.exists()
assert event["contexts"]["trace"]["origin"] == "manual"
rollback_spans = [
span
for span in event["spans"]
if span["data"].get(SPANDATA.DB_OPERATION) == SPANNAME.DB_ROLLBACK
]
assert len(rollback_spans) == 1
rollback_span = rollback_spans[0]
assert rollback_span["origin"] == "auto.db.django"
# Verify other database attributes
assert rollback_span["data"].get(SPANDATA.DB_SYSTEM) == "sqlite"
conn_params = connection.get_connection_params()
assert rollback_span["data"].get(SPANDATA.DB_NAME) is not None
assert rollback_span["data"].get(SPANDATA.DB_NAME) == conn_params.get(
"database"
) or conn_params.get("dbname")
insert_spans = [
span
for span in event["spans"]
if span["description"].startswith("INSERT INTO")
]
# Verify queries and rollback statements are siblings
for insert_span in insert_spans:
assert rollback_span["parent_span_id"] == insert_span["parent_span_id"]
sentry-python-2.64.0/tests/integrations/django/test_middleware.py 0000664 0000000 0000000 00000002053 15220673775 0025324 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
from sentry_sdk.integrations.django.middleware import _wrap_middleware
def _sync_capable_middleware_factory(sync_capable: "Optional[bool]") -> type:
"""Create a middleware class with a sync_capable attribute set to the value passed to the factory.
If the factory is called with None, the middleware class will not have a sync_capable attribute.
"""
sc = sync_capable # rename so we can set sync_capable in the class
class TestMiddleware:
nonlocal sc
if sc is not None:
sync_capable = sc
return TestMiddleware
@pytest.mark.parametrize(
("middleware", "sync_capable"),
(
(_sync_capable_middleware_factory(True), True),
(_sync_capable_middleware_factory(False), False),
(_sync_capable_middleware_factory(None), True),
),
)
def test_wrap_middleware_sync_capable_attribute(middleware, sync_capable):
wrapped_middleware = _wrap_middleware(middleware, "test_middleware")
assert wrapped_middleware.sync_capable is sync_capable
sentry-python-2.64.0/tests/integrations/django/test_tasks.py 0000664 0000000 0000000 00000021164 15220673775 0024340 0 ustar 00root root 0000000 0000000 import pytest
import sentry_sdk
from sentry_sdk.consts import OP
from sentry_sdk.integrations.django import DjangoIntegration
try:
from django.tasks import task
HAS_DJANGO_TASKS = True
except ImportError:
HAS_DJANGO_TASKS = False
@pytest.fixture
def immediate_backend(settings):
"""Configure Django to use the immediate task backend for synchronous testing."""
settings.TASKS = {
"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}
}
if HAS_DJANGO_TASKS:
@task
def simple_task():
return "result"
@task
def add_numbers(a, b):
return a + b
@task
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
@task
def failing_task():
raise ValueError("Task failed!")
@task
def task_one():
return 1
@task
def task_two():
return 2
@pytest.mark.skipif(
not HAS_DJANGO_TASKS,
reason="Django tasks are only available in Django 6.0+",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_task_span_is_created(
sentry_init,
capture_events,
capture_items,
immediate_backend,
span_streaming,
):
"""Test that the queue.submit.django span is created when a task is enqueued."""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
simple_task.enqueue()
sentry_sdk.flush()
spans = [item.payload for item in items]
queue_submit_spans = [
span
for span in spans
if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["name"]
== "tests.integrations.django.test_tasks.simple_task"
)
assert (
queue_submit_spans[0]["attributes"]["sentry.origin"] == "auto.http.django"
)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction"):
simple_task.enqueue()
(event,) = events
assert event["type"] == "transaction"
queue_submit_spans = [
span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["description"]
== "tests.integrations.django.test_tasks.simple_task"
)
assert queue_submit_spans[0]["origin"] == "auto.http.django"
@pytest.mark.skipif(
not HAS_DJANGO_TASKS,
reason="Django tasks are only available in Django 6.0+",
)
def test_task_enqueue_returns_result(sentry_init, immediate_backend):
"""Test that the task enqueuing behavior is unchanged from the user perspective."""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
result = add_numbers.enqueue(3, 5)
assert result is not None
assert result.return_value == 8
@pytest.mark.skipif(
not HAS_DJANGO_TASKS,
reason="Django tasks are only available in Django 6.0+",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_task_enqueue_with_kwargs(
sentry_init,
immediate_backend,
capture_events,
capture_items,
span_streaming,
):
"""Test that task enqueuing works correctly with keyword arguments."""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
result = greet.enqueue(name="World", greeting="Hi")
assert result.return_value == "Hi, World!"
sentry_sdk.flush()
spans = [item.payload for item in items]
queue_submit_spans = [
span
for span in spans
if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["name"]
== "tests.integrations.django.test_tasks.greet"
)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction"):
result = greet.enqueue(name="World", greeting="Hi")
assert result.return_value == "Hi, World!"
(event,) = events
queue_submit_spans = [
span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["description"]
== "tests.integrations.django.test_tasks.greet"
)
@pytest.mark.skipif(
not HAS_DJANGO_TASKS,
reason="Django tasks are only available in Django 6.0+",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_task_error_reporting(
sentry_init,
immediate_backend,
capture_events,
capture_items,
span_streaming,
):
"""Test that errors in tasks are correctly reported and don't break the span."""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
result = failing_task.enqueue()
with pytest.raises(ValueError, match="Task failed"):
_ = result.return_value
sentry_sdk.flush()
spans = [item.payload for item in items]
queue_submit_spans = [
span
for span in spans
if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["name"]
== "tests.integrations.django.test_tasks.failing_task"
)
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction"):
result = failing_task.enqueue()
with pytest.raises(ValueError, match="Task failed"):
_ = result.return_value
assert len(events) == 2
transaction_event = events[-1]
assert transaction_event["type"] == "transaction"
queue_submit_spans = [
span
for span in transaction_event["spans"]
if span["op"] == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 1
assert (
queue_submit_spans[0]["description"]
== "tests.integrations.django.test_tasks.failing_task"
)
@pytest.mark.skipif(
not HAS_DJANGO_TASKS,
reason="Django tasks are only available in Django 6.0+",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_multiple_task_enqueues_create_multiple_spans(
sentry_init,
capture_events,
capture_items,
immediate_backend,
span_streaming,
):
"""Test that enqueueing multiple tasks creates multiple spans."""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="custom parent"):
task_one.enqueue()
task_two.enqueue()
task_one.enqueue()
sentry_sdk.flush()
spans = [item.payload for item in items]
queue_submit_spans = [
span
for span in spans
if span["attributes"].get("sentry.op") == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 3
span_names = [span["name"] for span in queue_submit_spans]
else:
events = capture_events()
with sentry_sdk.start_transaction(name="test_transaction"):
task_one.enqueue()
task_two.enqueue()
task_one.enqueue()
(event,) = events
queue_submit_spans = [
span for span in event["spans"] if span["op"] == OP.QUEUE_SUBMIT_DJANGO
]
assert len(queue_submit_spans) == 3
span_names = [span["description"] for span in queue_submit_spans]
assert span_names.count("tests.integrations.django.test_tasks.task_one") == 2
assert span_names.count("tests.integrations.django.test_tasks.task_two") == 1
sentry-python-2.64.0/tests/integrations/django/test_transactions.py 0000664 0000000 0000000 00000011674 15220673775 0025730 0 ustar 00root root 0000000 0000000 from unittest import mock
import django
import pytest
from django.utils.translation import pgettext_lazy
# django<2.0 has only `url` with regex based patterns.
# django>=2.0 renames `url` to `re_path`, and additionally introduces `path`
# for new style URL patterns, e.g. .
if django.VERSION >= (2, 0):
from django.conf.urls import include
from django.urls import path, re_path
from django.urls.converters import PathConverter
else:
from django.conf.urls import include
from django.conf.urls import url as re_path
if django.VERSION < (1, 9):
included_url_conf = (re_path(r"^foo/bar/(?P[\w]+)", lambda x: ""),), "", ""
else:
included_url_conf = ((re_path(r"^foo/bar/(?P[\w]+)", lambda x: ""),), "")
from sentry_sdk.integrations.django.transactions import RavenResolver
example_url_conf = (
re_path(r"^api/(?P[\w_-]+)/store/$", lambda x: ""),
re_path(r"^api/(?P(v1|v2))/author/$", lambda x: ""),
re_path(
r"^api/(?P[^\/]+)/product/(?P(?:\d+|[A-Fa-f0-9-]{32,36}))/$",
lambda x: "",
),
re_path(r"^report/", lambda x: ""),
re_path(r"^example/", include(included_url_conf)),
)
def test_resolver_no_match():
resolver = RavenResolver()
result = resolver.resolve("/foo/bar", example_url_conf)
assert result is None
def test_resolver_re_path_complex_match():
resolver = RavenResolver()
result = resolver.resolve("/api/1234/store/", example_url_conf)
assert result == "/api/{project_id}/store/"
def test_resolver_re_path_complex_either_match():
resolver = RavenResolver()
result = resolver.resolve("/api/v1/author/", example_url_conf)
assert result == "/api/{version}/author/"
result = resolver.resolve("/api/v2/author/", example_url_conf)
assert result == "/api/{version}/author/"
def test_resolver_re_path_included_match():
resolver = RavenResolver()
result = resolver.resolve("/example/foo/bar/baz", example_url_conf)
assert result == "/example/foo/bar/{param}"
def test_resolver_re_path_multiple_groups():
resolver = RavenResolver()
result = resolver.resolve(
"/api/myproject/product/cb4ef1caf3554c34ae134f3c1b3d605f/", example_url_conf
)
assert result == "/api/{project_id}/product/{pid}/"
@pytest.mark.skipif(
django.VERSION < (2, 0),
reason="Django>=2.0 required for patterns",
)
def test_resolver_path_group():
url_conf = (path("api/v2//store/", lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/api/v2/1234/store/", url_conf)
assert result == "/api/v2/{project_id}/store/"
@pytest.mark.skipif(
django.VERSION < (2, 0),
reason="Django>=2.0 required for patterns",
)
def test_resolver_path_multiple_groups():
url_conf = (path("api/v2//product/", lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/api/v2/myproject/product/5689", url_conf)
assert result == "/api/v2/{project_id}/product/{pid}"
@pytest.mark.skipif(
django.VERSION < (2, 0),
reason="Django>=2.0 required for patterns",
)
@pytest.mark.skipif(
django.VERSION > (5, 1),
reason="get_converter removed in 5.1",
)
def test_resolver_path_complex_path_legacy():
class CustomPathConverter(PathConverter):
regex = r"[^/]+(/[^/]+){0,2}"
with mock.patch(
"django.urls.resolvers.get_converter",
return_value=CustomPathConverter,
):
url_conf = (path("api/v3/", lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/api/v3/abc/def/ghi", url_conf)
assert result == "/api/v3/{my_path}"
@pytest.mark.skipif(
django.VERSION < (5, 1),
reason="get_converters is used in 5.1",
)
def test_resolver_path_complex_path():
class CustomPathConverter(PathConverter):
regex = r"[^/]+(/[^/]+){0,2}"
with mock.patch(
"django.urls.resolvers.get_converters",
return_value={"custom_path": CustomPathConverter},
):
url_conf = (path("api/v3/", lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/api/v3/abc/def/ghi", url_conf)
assert result == "/api/v3/{my_path}"
@pytest.mark.skipif(
django.VERSION < (2, 0),
reason="Django>=2.0 required for patterns",
)
def test_resolver_path_no_converter():
url_conf = (path("api/v4/", lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/api/v4/myproject", url_conf)
assert result == "/api/v4/{project_id}"
@pytest.mark.skipif(
django.VERSION < (2, 0),
reason="Django>=2.0 required for path patterns",
)
def test_resolver_path_with_i18n():
url_conf = (path(pgettext_lazy("url", "pgettext"), lambda x: ""),)
resolver = RavenResolver()
result = resolver.resolve("/pgettext", url_conf)
assert result == "/pgettext"
sentry-python-2.64.0/tests/integrations/django/utils.py 0000664 0000000 0000000 00000001330 15220673775 0023305 0 ustar 00root root 0000000 0000000 from functools import partial
import pytest
import pytest_django
# Hack to prevent from experimental feature introduced in version `4.3.0` in `pytest-django` that
# requires explicit database allow from failing the test
pytest_mark_django_db_decorator = partial(pytest.mark.django_db)
try:
pytest_version = tuple(map(int, pytest_django.__version__.split(".")))
if pytest_version > (4, 2, 0):
pytest_mark_django_db_decorator = partial(
pytest.mark.django_db, databases="__all__"
)
except ValueError:
if "dev" in pytest_django.__version__:
pytest_mark_django_db_decorator = partial(
pytest.mark.django_db, databases="__all__"
)
except AttributeError:
pass
sentry-python-2.64.0/tests/integrations/dramatiq/ 0000775 0000000 0000000 00000000000 15220673775 0022136 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/dramatiq/__init__.py 0000664 0000000 0000000 00000000057 15220673775 0024251 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("dramatiq")
sentry-python-2.64.0/tests/integrations/dramatiq/test_dramatiq.py 0000664 0000000 0000000 00000035717 15220673775 0025366 0 ustar 00root root 0000000 0000000 import uuid
import dramatiq
import pytest
from dramatiq.brokers.stub import StubBroker
from dramatiq.middleware import Middleware, SkipMessage
import sentry_sdk
from sentry_sdk import start_transaction
from sentry_sdk.consts import SPANSTATUS
from sentry_sdk.integrations.dramatiq import DramatiqIntegration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.tracing import Transaction, TransactionSource
ignore_logger("dramatiq.worker.WorkerThread")
@pytest.fixture(scope="function")
def broker(request, sentry_init):
param = getattr(request, "param", None)
if isinstance(param, dict):
sentry_init(integrations=[DramatiqIntegration()], **param)
else:
sentry_init(
integrations=[DramatiqIntegration()],
traces_sample_rate=param,
)
broker = StubBroker()
broker.emit_after("process_boot")
dramatiq.set_broker(broker)
yield broker
broker.flush_all()
broker.close()
@pytest.fixture
def worker(broker):
worker = dramatiq.Worker(broker, worker_timeout=100, worker_threads=1)
worker.start()
yield worker
worker.stop()
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_a_single_error_is_captured(broker, worker, capture_events, fail_fast):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send(1, 2)
dummy_actor.send(1, 0)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
(event,) = events
exception = event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
@pytest.mark.parametrize(
"broker,expected_span_status,fail_fast,span_streaming",
[
({"traces_sample_rate": 1.0}, SPANSTATUS.INTERNAL_ERROR, False, False),
({"traces_sample_rate": 1.0}, SPANSTATUS.OK, False, False),
({"traces_sample_rate": 1.0}, SPANSTATUS.INTERNAL_ERROR, True, False),
({"traces_sample_rate": 1.0}, SPANSTATUS.OK, True, False),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
SPANSTATUS.INTERNAL_ERROR,
False,
True,
),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
SPANSTATUS.OK,
False,
True,
),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
SPANSTATUS.INTERNAL_ERROR,
True,
True,
),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
SPANSTATUS.OK,
True,
True,
),
],
ids=[
"error",
"success",
"error_fail_fast",
"success_fail_fast",
"error_stream",
"success_stream",
"error_fail_fast_stream",
"success_fail_fast_stream",
],
indirect=["broker"],
)
def test_task_transaction(
broker,
worker,
capture_events,
capture_items,
expected_span_status,
fail_fast,
span_streaming,
):
task_fails = expected_span_status == SPANSTATUS.INTERNAL_ERROR
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send(1, int(not task_fails))
if expected_span_status == SPANSTATUS.INTERNAL_ERROR and fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
sentry_sdk.flush()
if span_streaming:
if task_fails:
error_item, segment_item = items
error_event = error_item.payload
exception = error_event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == DramatiqIntegration.identifier
else:
(segment_item,) = items
segment = segment_item.payload
assert segment_item.type == "span"
assert segment["name"] == "dummy_actor"
assert segment["is_segment"] is True
assert segment["attributes"]["sentry.op"] == "queue.task.dramatiq"
assert segment["attributes"]["sentry.span.source"] == "task"
assert segment["status"] == ("error" if task_fails else "ok")
else:
if task_fails:
error_event = events.pop(0)
exception = error_event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == DramatiqIntegration.identifier
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "dummy_actor"
assert event["transaction_info"] == {"source": TransactionSource.TASK}
assert event["contexts"]["trace"]["status"] == expected_span_status
@pytest.mark.parametrize(
"broker,span_streaming",
[
({"traces_sample_rate": 1.0}, False),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
True,
),
],
ids=["static", "stream"],
indirect=["broker"],
)
def test_dramatiq_propagate_trace(
broker, worker, capture_events, capture_items, span_streaming
):
if span_streaming:
items = capture_items("span")
with sentry_sdk.traces.start_span(name="outer") as outer_span:
@dramatiq.actor(max_retries=0)
def propagated_trace_task():
pass
propagated_trace_task.send()
broker.join(propagated_trace_task.queue_name)
worker.join()
sentry_sdk.flush()
inner_segment, outer_segment = [i.payload for i in items]
assert inner_segment["name"] == "propagated_trace_task"
assert inner_segment["attributes"]["sentry.op"] == "queue.task.dramatiq"
assert inner_segment["trace_id"] == outer_span.trace_id
assert outer_segment["name"] == "outer"
else:
events = capture_events()
@dramatiq.actor(max_retries=0)
def propagated_trace_task():
pass
with start_transaction() as outer_transaction:
propagated_trace_task.send()
broker.join(propagated_trace_task.queue_name)
worker.join()
assert (
events[0]["transaction"] == "propagated_trace_task"
) # the "inner" transaction
assert events[0]["contexts"]["trace"]["trace_id"] == outer_transaction.trace_id
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_dramatiq_message_id_is_set_as_extra(
broker, worker, capture_events, fail_fast
):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
sentry_sdk.capture_message("hi")
return x / y
dummy_actor.send(1, 0)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
event_message, event_error = events
assert "dramatiq_message_id" in event_message["extra"]
assert "dramatiq_message_id" in event_error["extra"]
assert (
event_message["extra"]["dramatiq_message_id"]
== event_error["extra"]["dramatiq_message_id"]
)
msg_ids = [e["extra"]["dramatiq_message_id"] for e in events]
assert all(uuid.UUID(msg_id) and isinstance(msg_id, str) for msg_id in msg_ids)
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_local_variables_are_captured(broker, worker, capture_events, fail_fast):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
foo = 42 # noqa
return x / y
dummy_actor.send(1, 2)
dummy_actor.send(1, 0)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
(event,) = events
exception = event["exception"]["values"][0]
assert exception["stacktrace"]["frames"][-1]["vars"] == {
"x": "1",
"y": "0",
"foo": "42",
}
def test_that_messages_are_captured(broker, worker, capture_events):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor():
sentry_sdk.capture_message("hi")
dummy_actor.send()
broker.join(dummy_actor.queue_name)
worker.join()
(event,) = events
assert event["message"] == "hi"
assert event["level"] == "info"
assert event["transaction"] == "dummy_actor"
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_sub_actor_errors_are_captured(broker, worker, capture_events, fail_fast):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
sub_actor.send(x, y)
@dramatiq.actor(max_retries=0)
def sub_actor(x, y):
return x / y
dummy_actor.send(1, 2)
dummy_actor.send(1, 0)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
(event,) = events
assert event["transaction"] == "sub_actor"
exception = event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_multiple_errors_are_captured(broker, worker, capture_events, fail_fast):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send(1, 0)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
dummy_actor.send(1, None)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
event1, event2 = events
assert event1["transaction"] == "dummy_actor"
exception = event1["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
assert event2["transaction"] == "dummy_actor"
exception = event2["exception"]["values"][0]
assert exception["type"] == "TypeError"
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_message_data_is_added_as_request(
broker, worker, capture_events, fail_fast
):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send_with_options(
args=(
1,
0,
),
max_retries=0,
)
if fail_fast:
with pytest.raises(ZeroDivisionError):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
(event,) = events
assert event["transaction"] == "dummy_actor"
request_data = event["contexts"]["dramatiq"]["data"]
assert request_data["queue_name"] == "default"
assert request_data["actor_name"] == "dummy_actor"
assert request_data["args"] == [1, 0]
assert request_data["kwargs"] == {}
assert request_data["options"]["max_retries"] == 0
assert uuid.UUID(request_data["message_id"])
assert isinstance(request_data["message_timestamp"], int)
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_expected_exceptions_are_not_captured(
broker, worker, capture_events, fail_fast
):
events = capture_events()
class ExpectedException(Exception):
pass
@dramatiq.actor(max_retries=0, throws=ExpectedException)
def dummy_actor():
raise ExpectedException
dummy_actor.send()
if fail_fast:
with pytest.raises(ExpectedException):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
assert events == []
@pytest.mark.parametrize(
"fail_fast",
[
False,
True,
],
)
def test_that_retry_exceptions_are_not_captured(
broker, worker, capture_events, fail_fast
):
events = capture_events()
@dramatiq.actor(max_retries=2)
def dummy_actor():
raise dramatiq.errors.Retry("Retrying", delay=100)
dummy_actor.send()
if fail_fast:
with pytest.raises(dramatiq.errors.Retry):
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
else:
broker.join(dummy_actor.queue_name, fail_fast=fail_fast)
worker.join()
assert events == []
@pytest.mark.parametrize(
"broker,span_streaming",
[
({"traces_sample_rate": 1.0}, False),
(
{
"traces_sample_rate": 1.0,
"_experiments": {"trace_lifecycle": "stream"},
},
True,
),
],
ids=["static", "stream"],
indirect=["broker"],
)
def test_that_skip_message_cleans_up_scope_and_transaction(
broker, worker, capture_events, capture_items, span_streaming
):
captured_spans: list = []
class SkipMessageMiddleware(Middleware):
def before_process_message(self, broker, message):
if span_streaming:
captured_spans.append(sentry_sdk.get_current_span())
else:
captured_spans.append(sentry_sdk.get_current_scope().transaction)
raise SkipMessage()
broker.add_middleware(SkipMessageMiddleware())
if span_streaming:
items = capture_items("span")
@dramatiq.actor(max_retries=0)
def skipped_actor(): ...
skipped_actor.send()
broker.join(skipped_actor.queue_name)
worker.join()
if span_streaming:
sentry_sdk.flush()
(segment_payload,) = [i.payload for i in items]
assert segment_payload["name"] == "skipped_actor"
assert segment_payload["end_timestamp"] is not None
else:
(transaction,) = captured_spans
assert isinstance(transaction, Transaction)
assert transaction.timestamp is not None
sentry-python-2.64.0/tests/integrations/excepthook/ 0000775 0000000 0000000 00000000000 15220673775 0022505 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/excepthook/test_excepthook.py 0000664 0000000 0000000 00000004443 15220673775 0026274 0 ustar 00root root 0000000 0000000 import subprocess
import sys
from textwrap import dedent
import pytest
TEST_PARAMETERS = [("", "HttpTransport")]
if sys.version_info >= (3, 8):
TEST_PARAMETERS.append(('_experiments={"transport_http2": True}', "Http2Transport"))
@pytest.mark.parametrize("options, transport", TEST_PARAMETERS)
def test_excepthook(tmpdir, options, transport):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
from sentry_sdk import init, transport
def capture_envelope(self, envelope):
print("capture_envelope was called")
event = envelope.get_event()
if event is not None:
print(event)
transport.{transport}.capture_envelope = capture_envelope
init("http://foobar@localhost/123", {options})
frame_value = "LOL"
1/0
""".format(transport=transport, options=options)
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"capture_envelope was called" in output
@pytest.mark.parametrize("options, transport", TEST_PARAMETERS)
def test_always_value_excepthook(tmpdir, options, transport):
app = tmpdir.join("app.py")
app.write(
dedent(
"""
import sys
from sentry_sdk import init, transport
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
def capture_envelope(self, envelope):
print("capture_envelope was called")
event = envelope.get_event()
if event is not None:
print(event)
transport.{transport}.capture_envelope = capture_envelope
sys.ps1 = "always_value_test"
init("http://foobar@localhost/123",
integrations=[ExcepthookIntegration(always_run=True)],
{options}
)
frame_value = "LOL"
1/0
""".format(transport=transport, options=options)
)
)
with pytest.raises(subprocess.CalledProcessError) as excinfo:
subprocess.check_output([sys.executable, str(app)], stderr=subprocess.STDOUT)
output = excinfo.value.output
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"capture_envelope was called" in output
sentry-python-2.64.0/tests/integrations/falcon/ 0000775 0000000 0000000 00000000000 15220673775 0021576 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/falcon/__init__.py 0000664 0000000 0000000 00000000055 15220673775 0023707 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("falcon")
sentry-python-2.64.0/tests/integrations/falcon/test_falcon.py 0000664 0000000 0000000 00000051512 15220673775 0024455 0 ustar 00root root 0000000 0000000 import logging
import falcon
import falcon.testing
import pytest
import sentry_sdk
from sentry_sdk.integrations.falcon import FalconIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.utils import parse_version
try:
import falcon.asgi
except ImportError:
pass
else:
import falcon.inspect # We only need this module for the ASGI test
FALCON_VERSION = parse_version(falcon.__version__)
@pytest.fixture
def make_app(sentry_init):
def inner():
class MessageResource:
def on_get(self, req, resp):
sentry_sdk.capture_message("hi")
resp.media = "hi"
class MessageByIdResource:
def on_get(self, req, resp, message_id):
sentry_sdk.capture_message("hi")
resp.media = "hi"
class CustomError(Exception):
pass
class CustomErrorResource:
def on_get(self, req, resp):
raise CustomError()
def custom_error_handler(*args, **kwargs):
raise falcon.HTTPError(status=falcon.HTTP_400)
app = falcon.API()
app.add_route("/message", MessageResource())
app.add_route("/message/{message_id:int}", MessageByIdResource())
app.add_route("/custom-error", CustomErrorResource())
app.add_error_handler(CustomError, custom_error_handler)
return app
return inner
@pytest.fixture
def make_client(make_app):
def inner():
app = make_app()
return falcon.testing.TestClient(app)
return inner
@pytest.mark.parametrize("span_streaming", [True, False])
def test_has_context(
sentry_init,
capture_events,
capture_items,
make_client,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = make_client()
if span_streaming:
items = capture_items("event")
response = client.simulate_get("/message")
assert response.status == falcon.HTTP_200
(event,) = (item.payload for item in items)
else:
events = capture_events()
response = client.simulate_get("/message")
assert response.status == falcon.HTTP_200
(event,) = events
assert event["transaction"] == "/message" # Falcon URI template
assert "data" not in event["request"]
assert event["request"]["url"] == "http://falconframework.org/message"
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
("/message", "uri_template", "/message", "route"),
("/message", "path", "/message", "url"),
("/message/123456", "uri_template", "/message/{message_id:int}", "route"),
("/message/123456", "path", "/message/123456", "url"),
],
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_transaction_style(
sentry_init,
make_client,
capture_events,
capture_items,
url,
transaction_style,
expected_transaction,
expected_source,
span_streaming,
):
integration = FalconIntegration(transaction_style=transaction_style)
sentry_init(
integrations=[integration],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = make_client()
if span_streaming:
items = capture_items("event")
items = capture_items("event", "span")
response = client.simulate_get(url)
assert response.status == falcon.HTTP_200
(event,) = (item.payload for item in items if item.type == "event")
sentry_sdk.flush()
spans = [item.payload for item in items if item.type == "span"]
spans = [span for span in spans if span["name"] == expected_transaction]
assert len(spans) == 1
assert spans[0]["attributes"]["sentry.span.source"] == expected_source
else:
events = capture_events()
response = client.simulate_get(url)
assert response.status == falcon.HTTP_200
(event, transaction) = events
assert transaction["transaction"] == expected_transaction
assert transaction["transaction_info"] == {"source": expected_source}
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
@pytest.mark.parametrize("span_streaming", [True, False])
def test_unhandled_errors(
sentry_init,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_get(self, req, resp):
1 / 0
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
exceptions = capture_exceptions()
if span_streaming:
items = capture_items("event")
try:
client.simulate_get("/")
except ZeroDivisionError:
pass
(exc,) = exceptions
assert isinstance(exc, ZeroDivisionError)
(event,) = (item.payload for item in items)
else:
events = capture_events()
try:
client.simulate_get("/")
except ZeroDivisionError:
pass
(exc,) = exceptions
assert isinstance(exc, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "falcon"
assert " by zero" in event["exception"]["values"][0]["value"]
@pytest.mark.parametrize("span_streaming", [True, False])
def test_raised_5xx_errors(
sentry_init,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_502)
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
exceptions = capture_exceptions()
if span_streaming:
items = capture_items("event")
client.simulate_get("/")
(exc,) = exceptions
assert isinstance(exc, falcon.HTTPError)
(event,) = (item.payload for item in items)
else:
events = capture_events()
client.simulate_get("/")
(exc,) = exceptions
assert isinstance(exc, falcon.HTTPError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "falcon"
assert event["exception"]["values"][0]["type"] == "HTTPError"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_raised_4xx_errors(
sentry_init,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_400)
app = falcon.API()
app.add_route("/", Resource())
exceptions = capture_exceptions()
if span_streaming:
events = capture_items("event")
else:
events = capture_events()
client = falcon.testing.TestClient(app)
client.simulate_get("/")
assert len(exceptions) == 0
assert len(events) == 0
@pytest.mark.parametrize("span_streaming", [True, False])
def test_http_status(
sentry_init,
capture_exceptions,
capture_events,
capture_items,
span_streaming,
):
"""
This just demonstrates, that if Falcon raises a HTTPStatus with code 500
(instead of a HTTPError with code 500) Sentry will not capture it.
"""
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_get(self, req, resp):
raise falcon.http_status.HTTPStatus(falcon.HTTP_508)
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
exceptions = capture_exceptions()
if span_streaming:
events = capture_items("event")
client.simulate_get("/")
else:
events = capture_events()
client.simulate_get("/")
assert len(exceptions) == 0
assert len(events) == 0
@pytest.mark.parametrize("max_value_length", [1024, None])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_large_json_request(
sentry_init,
capture_events,
capture_items,
max_value_length,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
max_request_body_size="always",
max_value_length=max_value_length,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
data = {"foo": {"bar": "a" * (1034)}}
class Resource:
def on_post(self, req, resp):
assert req.media == data
sentry_sdk.capture_message("hi")
resp.media = "ok"
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
items = capture_items("event")
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = (item.payload for item in items)
else:
events = capture_events()
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = events
if max_value_length:
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {
"len": 1034,
"rem": [["!limit", "x", 1021, 1024]],
}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
else:
assert len(event["request"]["data"]["foo"]["bar"]) == 1034
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_empty_json_request(
sentry_init,
capture_events,
capture_items,
data,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_post(self, req, resp):
assert req.media == data
sentry_sdk.capture_message("hi")
resp.media = "ok"
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
items = capture_items("event")
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = (item.payload for item in items)
else:
events = capture_events()
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = events
assert event["request"]["data"] == data
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_raw_data_request(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
class Resource:
def on_post(self, req, resp):
sentry_sdk.capture_message("hi")
resp.media = "ok"
app = falcon.API()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
items = capture_items("event")
response = client.simulate_post("/", body="hi")
assert response.status == falcon.HTTP_200
(event,) = (item.payload for item in items)
else:
events = capture_events()
response = client.simulate_post("/", body="hi")
assert response.status == falcon.HTTP_200
(event,) = events
assert event["request"]["headers"]["Content-Length"] == "2"
assert event["request"]["data"] == ""
@pytest.mark.parametrize("span_streaming", [True, False])
def test_logging(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration(), LoggingIntegration(event_level="ERROR")],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
logger = logging.getLogger()
app = falcon.API()
class Resource:
def on_get(self, req, resp):
logger.error("hi")
resp.media = "ok"
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
items = capture_items("event")
client.simulate_get("/")
(event,) = (item.payload for item in items)
else:
events = capture_events()
client.simulate_get("/")
(event,) = events
assert event["level"] == "error"
def test_500(sentry_init):
sentry_init(integrations=[FalconIntegration()])
app = falcon.API()
class Resource:
def on_get(self, req, resp):
1 / 0
app.add_route("/", Resource())
def http500_handler(ex, req, resp, params):
sentry_sdk.capture_exception(ex)
resp.media = {"message": "Sentry error."}
app.add_error_handler(Exception, http500_handler)
client = falcon.testing.TestClient(app)
response = client.simulate_get("/")
assert response.json == {"message": "Sentry error."}
@pytest.mark.parametrize("span_streaming", [True, False])
def test_error_in_errorhandler(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
app = falcon.API()
class Resource:
def on_get(self, req, resp):
raise ValueError()
app.add_route("/", Resource())
def http500_handler(ex, req, resp, params):
1 / 0
app.add_error_handler(Exception, http500_handler)
client = falcon.testing.TestClient(app)
if span_streaming:
items = capture_items("event")
with pytest.raises(ZeroDivisionError):
client.simulate_get("/")
(event,) = (item.payload for item in items)
else:
events = capture_events()
with pytest.raises(ZeroDivisionError):
client.simulate_get("/")
(event,) = events
last_ex_values = event["exception"]["values"][-1]
assert last_ex_values["type"] == "ZeroDivisionError"
assert last_ex_values["stacktrace"]["frames"][-1]["vars"]["ex"] == "ValueError()"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_bad_request_not_captured(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
app = falcon.API()
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPBadRequest()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
events = capture_items("event")
else:
events = capture_events()
client.simulate_get("/")
assert not events
@pytest.mark.parametrize("span_streaming", [True, False])
def test_does_not_leak_scope(
sentry_init,
capture_events,
capture_items,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
app = falcon.API()
class Resource:
def on_get(self, req, resp):
sentry_sdk.get_isolation_scope().set_tag("request_data", True)
def generator():
for row in range(1000):
assert sentry_sdk.get_isolation_scope()._tags["request_data"]
yield (str(row) + "\n").encode()
resp.stream = generator()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
if span_streaming:
events = capture_items("event")
else:
events = capture_events()
sentry_sdk.get_isolation_scope().set_tag("request_data", False)
response = client.simulate_get("/")
expected_response = "".join(str(row) + "\n" for row in range(1000))
assert response.text == expected_response
assert not events
assert not sentry_sdk.get_isolation_scope()._tags["request_data"]
@pytest.mark.skipif(
not hasattr(falcon, "asgi"), reason="This Falcon version lacks ASGI support."
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_not_breaking_asgi(sentry_init, span_streaming):
"""
This test simply verifies that the Falcon integration does not break ASGI
Falcon apps.
The test does not verify ASGI Falcon support, since our Falcon integration
currently lacks support for ASGI Falcon apps.
"""
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
asgi_app = falcon.asgi.App()
try:
falcon.inspect.inspect_app(asgi_app)
except TypeError:
pytest.fail("Falcon integration causing errors in ASGI apps.")
@pytest.mark.skipif(
(FALCON_VERSION or ()) < (3,),
reason="The Sentry Falcon integration only supports custom error handlers on Falcon 3+",
)
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_custom_error_handler(
sentry_init,
make_app,
capture_events,
capture_items,
span_streaming,
):
"""
When a custom error handler handles what otherwise would have resulted in a 5xx error,
changing the HTTP status to a non-5xx status, no error event should be sent to Sentry.
"""
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
app = make_app()
client = falcon.testing.TestClient(app)
if span_streaming:
events = capture_items("event")
else:
events = capture_events()
client.simulate_get("/custom-error")
assert len(events) == 0
@pytest.mark.parametrize("span_streaming", [True, False])
def test_span_origin(
sentry_init,
capture_events,
capture_items,
make_client,
span_streaming,
):
sentry_init(
integrations=[FalconIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
client = make_client()
if span_streaming:
items = capture_items("span")
client.simulate_get("/message")
sentry_sdk.flush()
spans = [item.payload for item in items]
assert spans[0]["attributes"]["sentry.origin"] == "auto.http.falcon"
else:
events = capture_events()
client.simulate_get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.falcon"
@pytest.mark.parametrize("span_streaming", [True, False])
def test_falcon_request_media(sentry_init, span_streaming):
# test_passed stores whether the test has passed.
test_passed = False
# test_failure_reason stores the reason why the test failed
# if test_passed is False. The value is meaningless when
# test_passed is True.
test_failure_reason = "test endpoint did not get called"
class SentryCaptureMiddleware:
def process_request(self, _req, _resp):
# This capture message forces Falcon event processors to run
# before the request handler runs
sentry_sdk.capture_message("Processing request")
class RequestMediaResource:
def on_post(self, req, _):
nonlocal test_passed, test_failure_reason
raw_data = req.bounded_stream.read()
# If the raw_data is empty, the request body stream
# has been exhausted by the SDK. Test should fail in
# this case.
test_passed = raw_data != b""
test_failure_reason = "request body has been read"
sentry_init(
integrations=[FalconIntegration()],
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
)
try:
app_class = falcon.App # Falcon ≥3.0
except AttributeError:
app_class = falcon.API # Falcon <3.0
app = app_class(middleware=[SentryCaptureMiddleware()])
app.add_route("/read_body", RequestMediaResource())
client = falcon.testing.TestClient(app)
client.simulate_post("/read_body", json={"foo": "bar"})
# Check that simulate_post actually calls the resource, and
# that the SDK does not exhaust the request body stream.
assert test_passed, test_failure_reason
sentry-python-2.64.0/tests/integrations/fastapi/ 0000775 0000000 0000000 00000000000 15220673775 0021763 5 ustar 00root root 0000000 0000000 sentry-python-2.64.0/tests/integrations/fastapi/__init__.py 0000664 0000000 0000000 00000000056 15220673775 0024075 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("fastapi")
sentry-python-2.64.0/tests/integrations/fastapi/photo.jpg 0000664 0000000 0000000 00000051026 15220673775 0023622 0 ustar 00root root 0000000 0000000 JFIF H H C
C
1 x88\Ld
W-)yk\=mdPm';.6[aƠp @-'MMkGVL ElHij.T\j:#ERpݖ-LI(MX