[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(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
except (TypeError, ValueError, AttributeError):
return None
return release_tuple
def _is_contextvars_broken():
# type: () -> 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) -> type
class ContextVar:
# Super-limited impl of ContextVar
def __init__(self, name, default=None):
# type: (str, Any) -> None
self._name = name
self._default = default
self._local = local()
self._original_local = local()
def get(self, default=None):
# type: (Any) -> Any
return getattr(self._local, "value", default or self._default)
def set(self, value):
# type: (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):
# type: (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():
# type: () -> 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.
"""
def qualname_from_function(func):
# type: (Callable[..., Any]) -> Optional[str]
"""Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
func_qualname = None # type: Optional[str]
# Python 2
try:
return "%s.%s.%s" % (
func.im_class.__module__, # type: ignore
func.im_class.__name__, # type: ignore
func.__name__,
)
except Exception:
pass
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__"): # Python 2.7 has no __qualname__
func_qualname = func.__name__
# Python 3: methods, functions, classes
if func_qualname is not None:
if hasattr(func, "__module__"):
func_qualname = func.__module__ + "." + func_qualname
func_qualname = prefix + func_qualname + suffix
return func_qualname
def transaction_from_function(func):
# type: (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, configured_timeout):
# type: (float, int) -> None
threading.Thread.__init__(self)
self.waiting_time = waiting_time
self.configured_timeout = configured_timeout
self._stop_event = threading.Event()
def stop(self):
# type: () -> None
self._stop_event.set()
def run(self):
# type: () -> 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
raise ServerlessTimeoutWarning(
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
integer_configured_timeout
)
)
def to_base64(original):
# type: (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):
# type: (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, remove_authority=True, remove_query_values=True, split=False):
# type: (str, bool, bool, bool) -> 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, sanitize=True):
# type: (str, bool) -> 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, source):
# type: (Any, 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, regex_list=None, substring_matching=False):
# type: (str, Optional[List[str]], bool) -> 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, url):
# type: (sentry_sdk.client.BaseClient, 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():
# type: () -> Iterator[Tuple[str, str]]
try:
from importlib import metadata
yielded = set()
for dist in metadata.distributions():
name = dist.metadata["Name"]
# `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):
# type: (str) -> str
return name.lower()
def _get_installed_modules():
# type: () -> Dict[str, str]
global _installed_modules
if _installed_modules is None:
_installed_modules = dict(_generate_installed_modules())
return _installed_modules
def package_version(package):
# type: (str) -> Optional[Tuple[int, ...]]
installed_packages = _get_installed_modules()
version = installed_packages.get(package)
if version is None:
return None
return parse_version(version)
def reraise(tp, value, tb=None):
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> NoReturn
assert value is not None
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
def _no_op(*_a, **_k):
# type: (*Any, **Any) -> None
"""No-op function for ensure_integration_enabled."""
pass
if TYPE_CHECKING:
@overload
def ensure_integration_enabled(
integration, # type: type[sentry_sdk.integrations.Integration]
original_function, # type: Callable[P, R]
):
# type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
...
@overload
def ensure_integration_enabled(
integration, # type: type[sentry_sdk.integrations.Integration]
):
# type: (...) -> Callable[[Callable[P, None]], Callable[P, None]]
...
def ensure_integration_enabled(
integration, # type: type[sentry_sdk.integrations.Integration]
original_function=_no_op, # type: Union[Callable[P, R], Callable[P, None]]
):
# type: (...) -> 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):
# type: (Callable[P, R]) -> Callable[P, R]
def runner(*args: "P.args", **kwargs: "P.kwargs"):
# type: (...) -> 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():
# type: () -> int
return time.perf_counter_ns()
else:
def nanosecond_time():
# type: () -> int
return int(time.perf_counter() * 1e9)
def now():
# type: () -> 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(): # type: ignore[misc]
# type: () -> Optional[Hub]
return None
def is_module_patched(mod_name):
# type: (str) -> bool
# unable to import from gevent means no modules have been patched
return False
def is_gevent():
# type: () -> bool
return is_module_patched("threading") or is_module_patched("_thread")
def get_current_thread_meta(thread=None):
# type: (Optional[threading.Thread]) -> 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
sentry-python-2.18.0/sentry_sdk/worker.py 0000664 0000000 0000000 00000010560 14712146540 0020523 0 ustar 00root root 0000000 0000000 import os
import threading
from time import sleep, time
from sentry_sdk._queue import Queue, FullError
from sentry_sdk.utils import logger
from sentry_sdk.consts import DEFAULT_QUEUE_SIZE
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
from typing import Optional
from typing import Callable
_TERMINATOR = object()
class BackgroundWorker:
def __init__(self, queue_size=DEFAULT_QUEUE_SIZE):
# type: (int) -> None
self._queue = Queue(queue_size) # type: Queue
self._lock = threading.Lock()
self._thread = None # type: Optional[threading.Thread]
self._thread_for_pid = None # type: Optional[int]
@property
def is_alive(self):
# type: () -> 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):
# type: () -> None
if not self.is_alive:
self.start()
def _timed_queue_join(self, timeout):
# type: (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):
# type: () -> 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):
# type: () -> 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, callback=None):
# type: (float, Optional[Any]) -> 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):
# type: () -> bool
return self._queue.full()
def _wait_flush(self, timeout, callback):
# type: (float, 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):
# type: (Callable[[], None]) -> bool
self._ensure_thread()
try:
self._queue.put_nowait(callback)
return True
except FullError:
return False
def _target(self):
# type: () -> 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)
sentry-python-2.18.0/setup.py 0000664 0000000 0000000 00000007527 14712146540 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 setup, find_packages
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.18.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="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.*"],
"httpx": ["httpx>=0.16.0"],
"huey": ["huey>=2"],
"huggingface_hub": ["huggingface_hub>=0.22"],
"langchain": ["langchain>=0.0.210"],
"launchdarkly": ["launchdarkly-server-sdk>=9.8.0"],
"litestar": ["litestar>=2.0.0"],
"loguru": ["loguru>=0.5"],
"openai": ["openai>=1.0.0", "tiktoken>=0.3.0"],
"openfeature": ["openfeature-sdk>=0.7.1"],
"opentelemetry": ["opentelemetry-distro>=0.35b0"],
"opentelemetry-experimental": ["opentelemetry-distro"],
"pure_eval": ["pure_eval", "executing", "asttokens"],
"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"],
"tornado": ["tornado>=6"],
},
entry_points={
"opentelemetry_propagator": [
"sentry=sentry_sdk.integrations.opentelemetry:SentryPropagator"
]
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"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",
"Topic :: Software Development :: Libraries :: Python Modules",
],
options={"bdist_wheel": {"universal": "1"}},
)
sentry-python-2.18.0/tests/ 0000775 0000000 0000000 00000000000 14712146540 0015613 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/__init__.py 0000664 0000000 0000000 00000000661 14712146540 0017727 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.18.0/tests/conftest.py 0000664 0000000 0000000 00000044747 14712146540 0020032 0 ustar 00root root 0000000 0000000 import json
import os
import socket
import warnings
from threading import Thread
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, HTTPServer
from unittest import mock
import pytest
import jsonschema
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
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 reraise
from tests import _warning_recorder, _warning_recorder_mgr
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Optional
from collections.abc import Iterator
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)
try:
import pytest_benchmark
except ImportError:
@pytest.fixture
def benchmark():
return lambda x: x()
else:
del pytest_benchmark
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
raise AssertionError(warning)
@pytest.fixture
def validate_event_schema(tmpdir):
def inner(event):
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 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
@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
@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
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(event):
assert event["type"] == "transaction"
by_parent = {}
for span in event["spans"]:
by_parent.setdefault(span["parent_span_id"], []).append(span)
def render_span(span):
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)
root_span = event["contexts"]["trace"]
# Return a list instead of a multiline string because black will know better how to format that
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
class MockServerRequestHandler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802
# Process an HTTP GET request and return a response with an HTTP 200 status.
self.send_response(200)
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.setDaemon(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=False):
# type: (bool) -> 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)
sentry-python-2.18.0/tests/integrations/ 0000775 0000000 0000000 00000000000 14712146540 0020321 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0022420 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/aiohttp/ 0000775 0000000 0000000 00000000000 14712146540 0021771 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/aiohttp/__init__.py 0000664 0000000 0000000 00000000056 14712146540 0024103 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("aiohttp")
sentry-python-2.18.0/tests/integrations/aiohttp/test_aiohttp.py 0000664 0000000 0000000 00000047644 14712146540 0025071 0 ustar 00root root 0000000 0000000 import asyncio
import json
from contextlib import suppress
from unittest import mock
import pytest
from aiohttp import web, ClientSession
from aiohttp.client import ServerDisconnectedError
from aiohttp.web_request import Request
from aiohttp.web_exceptions import (
HTTPInternalServerError,
HTTPNetworkAuthenticationRequired,
HTTPBadRequest,
HTTPNotFound,
HTTPUnavailableForLegalReasons,
)
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
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, loop, 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.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 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-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_span_origin(
sentry_init,
aiohttp_client,
capture_events,
):
sentry_init(
integrations=[AioHttpIntegration()],
traces_sample_rate=1.0,
)
async def hello(request):
async with ClientSession() as session:
async with session.get("http://example.com"):
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"
sentry-python-2.18.0/tests/integrations/anthropic/ 0000775 0000000 0000000 00000000000 14712146540 0022310 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/anthropic/__init__.py 0000664 0000000 0000000 00000000060 14712146540 0024415 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("anthropic")
sentry-python-2.18.0/tests/integrations/anthropic/test_anthropic.py 0000664 0000000 0000000 00000057351 14712146540 0025723 0 ustar 00root root 0000000 0000000 from unittest import mock
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)
import pytest
from anthropic import AsyncAnthropic, Anthropic, AnthropicError, AsyncStream, Stream
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
from sentry_sdk.utils import package_version
try:
from anthropic.types import InputJSONDelta
except ImportError:
try:
from anthropic.types import InputJsonDelta as InputJSONDelta
except ImportError:
pass
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_transaction
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.integrations.anthropic import AnthropicIntegration
ANTHROPIC_VERSION = package_version("anthropic")
EXAMPLE_MESSAGE = Message(
id="id",
model="model",
role="assistant",
content=[TextBlock(type="text", text="Hi, I'm Claude.")],
type="message",
usage=Usage(input_tokens=10, output_tokens=20),
)
async def async_iterator(values):
for value in values:
yield value
@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, send_default_pii, include_prompts
):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"type": "text", "text": "Hi, I'm Claude."}
]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
assert span["data"]["ai.streaming"] is 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, send_default_pii, include_prompts
):
sentry_init(
integrations=[AnthropicIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"type": "text", "text": "Hi, I'm Claude."}
]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
assert span["data"]["ai.streaming"] is 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, send_default_pii, include_prompts
):
client = Anthropic(api_key="z")
returned_stream = Stream(cast_to=None, response=None, client=client)
returned_stream._iterator = [
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,
)
events = capture_events()
client.messages._post = mock.Mock(return_value=returned_stream)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
with start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
assert message == returned_stream
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"type": "text", "text": "Hi! I'm Claude!"}
]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 30
assert span["measurements"]["ai_total_tokens_used"]["value"] == 40
assert span["data"]["ai.streaming"] is True
@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, send_default_pii, include_prompts
):
client = AsyncAnthropic(api_key="z")
returned_stream = AsyncStream(cast_to=None, response=None, client=client)
returned_stream._iterator = async_iterator(
[
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,
)
events = capture_events()
client.messages._post = AsyncMock(return_value=returned_stream)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
with start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
assert message == returned_stream
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"type": "text", "text": "Hi! I'm Claude!"}
]
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 30
assert span["measurements"]["ai_total_tokens_used"]["value"] == 40
assert span["data"]["ai.streaming"] is True
@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, send_default_pii, include_prompts
):
client = Anthropic(api_key="z")
returned_stream = Stream(cast_to=None, response=None, client=client)
returned_stream._iterator = [
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,
)
events = capture_events()
client.messages._post = mock.Mock(return_value=returned_stream)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
with start_transaction(name="anthropic"):
message = client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
for _ in message:
pass
assert message == returned_stream
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"text": "", "type": "text"}
] # we do not record InputJSONDelta because it could contain PII
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 366
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 51
assert span["measurements"]["ai_total_tokens_used"]["value"] == 417
assert span["data"]["ai.streaming"] is True
@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, send_default_pii, include_prompts
):
client = AsyncAnthropic(api_key="z")
returned_stream = AsyncStream(cast_to=None, response=None, client=client)
returned_stream._iterator = async_iterator(
[
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,
)
events = capture_events()
client.messages._post = AsyncMock(return_value=returned_stream)
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
}
]
with start_transaction(name="anthropic"):
message = await client.messages.create(
max_tokens=1024, messages=messages, model="model", stream=True
)
async for _ in message:
pass
assert message == returned_stream
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.ANTHROPIC_MESSAGES_CREATE
assert span["description"] == "Anthropic messages create"
assert span["data"][SPANDATA.AI_MODEL_ID] == "model"
if send_default_pii and include_prompts:
assert span["data"][SPANDATA.AI_INPUT_MESSAGES] == messages
assert span["data"][SPANDATA.AI_RESPONSES] == [
{"text": "", "type": "text"}
] # we do not record InputJSONDelta because it could contain PII
else:
assert SPANDATA.AI_INPUT_MESSAGES not in span["data"]
assert SPANDATA.AI_RESPONSES not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 366
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 51
assert span["measurements"]["ai_total_tokens_used"]["value"] == 417
assert span["data"]["ai.streaming"] is True
def test_exception_message_create(sentry_init, capture_events):
sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0)
events = capture_events()
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,
)
(event,) = events
assert event["level"] == "error"
@pytest.mark.asyncio
async def test_exception_message_create_async(sentry_init, capture_events):
sentry_init(integrations=[AnthropicIntegration()], traces_sample_rate=1.0)
events = capture_events()
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,
)
(event,) = events
assert event["level"] == "error"
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = Anthropic(api_key="z")
client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
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"
@pytest.mark.asyncio
async def test_span_origin_async(sentry_init, capture_events):
sentry_init(
integrations=[AnthropicIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = AsyncAnthropic(api_key="z")
client.messages._post = AsyncMock(return_value=EXAMPLE_MESSAGE)
messages = [
{
"role": "user",
"content": "Hello, Claude",
}
]
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"
sentry-python-2.18.0/tests/integrations/argv/ 0000775 0000000 0000000 00000000000 14712146540 0021260 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/argv/test_argv.py 0000664 0000000 0000000 00000000644 14712146540 0023634 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.18.0/tests/integrations/ariadne/ 0000775 0000000 0000000 00000000000 14712146540 0021724 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/ariadne/__init__.py 0000664 0000000 0000000 00000000152 14712146540 0024033 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("ariadne")
pytest.importorskip("fastapi")
pytest.importorskip("flask")
sentry-python-2.18.0/tests/integrations/ariadne/test_ariadne.py 0000664 0000000 0000000 00000016504 14712146540 0024746 0 ustar 00root root 0000000 0000000 from ariadne import gql, graphql_sync, ObjectType, QueryType, make_executable_schema
from ariadne.asgi import GraphQL
from fastapi import FastAPI
from fastapi.testclient import TestClient
from flask import Flask, request, jsonify
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.18.0/tests/integrations/arq/ 0000775 0000000 0000000 00000000000 14712146540 0021104 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/arq/__init__.py 0000664 0000000 0000000 00000000052 14712146540 0023212 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("arq")
sentry-python-2.18.0/tests/integrations/arq/test_arq.py 0000664 0000000 0000000 00000020410 14712146540 0023275 0 ustar 00root root 0000000 0000000 import asyncio
import pytest
from sentry_sdk import get_client, start_transaction
from sentry_sdk.integrations.arq import ArqIntegration
import arq.worker
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
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(
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,
)
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.mark.asyncio
async def test_job_result(init_arq):
async def increase(ctx, num):
return num + 1
increase.__qualname__ = increase.__name__
pool, worker = init_arq([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
async def test_job_retry(capture_events, init_arq):
async def retry_job(ctx):
if ctx["job_try"] < 2:
raise arq.worker.Retry
retry_job.__qualname__ = retry_job.__name__
pool, worker = init_arq([retry_job])
job = await pool.enqueue_job("retry_job")
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.asyncio
async def test_job_transaction(capture_events, init_arq, source, job_fails):
async def division(_, a, b=0):
return a / b
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_arq(**{functions_key: [division], cron_jobs_key: [cron_job]})
events = capture_events()
job = await pool.enqueue_job("division", 1, b=int(not job_fails))
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.asyncio
async def test_enqueue_job(capture_events, init_arq, source):
async def dummy_job(_):
pass
pool, _ = init_arq(**{source: [dummy_job]})
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
async def test_execute_job_without_integration(init_arq):
async def dummy_job(_ctx):
pass
dummy_job.__qualname__ = dummy_job.__name__
pool, worker = init_arq([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.asyncio
async def test_span_origin_producer(capture_events, init_arq, source):
async def dummy_job(_):
pass
pool, _ = init_arq(**{source: [dummy_job]})
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
async def test_span_origin_consumer(capture_events, init_arq):
async def job(ctx):
pass
job.__qualname__ = job.__name__
pool, worker = init_arq([job])
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"
sentry-python-2.18.0/tests/integrations/asgi/ 0000775 0000000 0000000 00000000000 14712146540 0021244 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asgi/__init__.py 0000664 0000000 0000000 00000000201 14712146540 0023346 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("asyncio")
pytest.importorskip("pytest_asyncio")
pytest.importorskip("async_asgi_testclient")
sentry-python-2.18.0/tests/integrations/asgi/test_asgi.py 0000664 0000000 0000000 00000046670 14712146540 0023615 0 ustar 00root root 0000000 0000000 from collections import Counter
import pytest
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations._asgi_common import _get_ip, _get_headers
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware, _looks_like_asgi3
from async_asgi_testclient import TestClient
@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="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
async def test_capture_transaction(
sentry_init,
asgi3_app,
capture_events,
):
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
app = SentryAsgiMiddleware(asgi3_app)
async with TestClient(app) as client:
events = capture_events()
await client.get("/some_url?somevalue=123")
(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
async def test_capture_transaction_with_error(
sentry_init,
asgi3_app_with_error,
capture_events,
DictionaryContaining, # noqa: N803
):
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
app = SentryAsgiMiddleware(asgi3_app_with_error)
events = capture_events()
with pytest.raises(ZeroDivisionError):
async with TestClient(app) as client:
await client.get("/some_url")
(
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"
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
async def test_has_trace_if_performance_enabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init(traces_sample_rate=1.0)
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, 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.asyncio
async def test_trace_from_headers_if_performance_enabled(
sentry_init,
asgi3_app_with_error_and_msg,
capture_events,
):
sentry_init(traces_sample_rate=1.0)
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, 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
async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request):
sentry_init(send_default_pii=True)
events = capture_events()
asgi3_ws_app = SentryAsgiMiddleware(asgi3_ws_app)
scope = {
"type": "websocket",
"endpoint": asgi3_app,
"client": ("127.0.0.1", 60457),
"route": "some_url",
"headers": [
("accept", "*/*"),
],
}
with pytest.raises(ValueError):
async with TestClient(asgi3_ws_app, scope=scope) as client:
async with client.websocket_connect("/ws") as ws:
await ws.receive_text()
msg_event, error_event = events
assert msg_event["message"] == "Some message to the world!"
(exc,) = error_event["exception"]["values"]
assert exc["type"] == "ValueError"
assert exc["value"] == "Oh no"
@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.asyncio
async def test_transaction_style(
sentry_init,
asgi3_app,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
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:
events = capture_events()
await client.get(url)
(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",
),
],
)
async def test_transaction_name(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
asgi3_app,
capture_envelopes,
):
"""
Tests that the transaction name is something meaningful.
"""
sentry_init(
traces_sample_rate=1.0,
)
envelopes = capture_envelopes()
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
async with TestClient(app) as client:
await client.get(request_url)
(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",
),
],
)
async def test_transaction_name_in_traces_sampler(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
asgi3_app,
):
"""
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):
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,
)
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
async with TestClient(app) as client:
await client.get(request_url)
@pytest.mark.asyncio
async def test_custom_transaction_name(
sentry_init, asgi3_custom_transaction_app, capture_events
):
sentry_init(traces_sample_rate=1.0)
events = capture_events()
app = SentryAsgiMiddleware(asgi3_custom_transaction_app)
async with TestClient(app) as client:
await client.get("/test")
(transaction_event,) = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "foobar"
assert transaction_event["transaction_info"] == {"source": "custom"}
sentry-python-2.18.0/tests/integrations/asyncio/ 0000775 0000000 0000000 00000000000 14712146540 0021766 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asyncio/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0024065 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asyncio/test_asyncio.py 0000664 0000000 0000000 00000025617 14712146540 0025057 0 ustar 00root root 0000000 0000000 import asyncio
import inspect
import sys
from unittest.mock import MagicMock, patch
import pytest
import sentry_sdk
from sentry_sdk.consts import OP
from sentry_sdk.integrations.asyncio import AsyncioIntegration, patch_asyncio
try:
from contextvars import Context, ContextVar
except ImportError:
pass # All tests will be skipped with incompatible versions
minimum_python_37 = pytest.mark.skipif(
sys.version_info < (3, 7), reason="Asyncio tests need Python >= 3.7"
)
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
@pytest.fixture(scope="session")
def event_loop(request):
"""Create an instance of the default event loop for each test case."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
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_37
@pytest.mark.asyncio
async def test_create_task(
sentry_init,
capture_events,
event_loop,
):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[
AsyncioIntegration(),
],
)
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"):
tasks = [event_loop.create_task(foo()), event_loop.create_task(bar())]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
(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_37
@pytest.mark.asyncio
async def test_gather(
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_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()
(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_37
@pytest.mark.asyncio
async def test_exception(
sentry_init,
capture_events,
event_loop,
):
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 = [event_loop.create_task(boom()), event_loop.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_37
@pytest.mark.asyncio
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
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_37
@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
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_37
@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_37
@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
# 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
# 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_37
@pytest.mark.asyncio
async def test_span_origin(
sentry_init,
capture_events,
event_loop,
):
sentry_init(
integrations=[AsyncioIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
with sentry_sdk.start_transaction(name="something"):
tasks = [
event_loop.create_task(foo()),
]
await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
sentry_sdk.flush()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.function.asyncio"
sentry-python-2.18.0/tests/integrations/asyncpg/ 0000775 0000000 0000000 00000000000 14712146540 0021765 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asyncpg/__init__.py 0000664 0000000 0000000 00000000504 14712146540 0024075 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.18.0/tests/integrations/asyncpg/asyncpg_helpers/ 0000775 0000000 0000000 00000000000 14712146540 0025153 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asyncpg/asyncpg_helpers/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0027252 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/asyncpg/asyncpg_helpers/helpers.py 0000664 0000000 0000000 00000000136 14712146540 0027167 0 ustar 00root root 0000000 0000000 async def execute_query_in_connection(query, connection):
await connection.execute(query)
sentry-python-2.18.0/tests/integrations/asyncpg/test_asyncpg.py 0000664 0000000 0000000 00000052721 14712146540 0025051 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=foo -e POSTGRES_PASSWORD=bar -d -p 5432:5432 postgres
```
The tests use the following credentials to establish a database connection.
"""
import os
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 = os.getenv("SENTRY_PYTHON_TEST_POSTGRES_NAME", "postgres")
import datetime
from contextlib import contextmanager
from unittest import mock
import asyncpg
import pytest
import pytest_asyncio
from asyncpg import connect, Connection
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.integrations.asyncpg import AsyncPGIntegration
from sentry_sdk.consts import SPANDATA
from sentry_sdk.tracing_utils import record_sql_queries
from tests.conftest import ApproxDict
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,
"server.address": PG_HOST,
"server.port": PG_PORT,
}
),
"message": "connect",
"type": "default",
}
@pytest_asyncio.fixture(autouse=True)
async def _clean_pg():
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
async def test_connect(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.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=[AsyncPGIntegration()],
_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 ('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")
(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($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
async def test_execute_many(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)),
],
)
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",
},
]
@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": {},
"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": {},
"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();\n"
"CLOSE ALL;\n"
"UNLISTEN *;\n"
"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();\n"
"CLOSE ALL;\n"
"UNLISTEN *;\n"
"RESET ALL;",
"type": "default",
},
]
@pytest.mark.asyncio
async def test_query_source_disabled(sentry_init, capture_events):
sentry_options = {
"integrations": [AsyncPGIntegration()],
"enable_tracing": True,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
}
sentry_init(**sentry_options)
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])
async def test_query_source_enabled(
sentry_init, capture_events, enable_db_query_source
):
sentry_options = {
"integrations": [AsyncPGIntegration()],
"enable_tracing": True,
"db_query_source_threshold_ms": 0,
}
if enable_db_query_source is not None:
sentry_options["enable_db_query_source"] = enable_db_query_source
sentry_init(**sentry_options)
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 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_query_source(sentry_init, capture_events):
sentry_init(
integrations=[AsyncPGIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
)
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 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"
@pytest.mark.asyncio
async def test_query_source_with_module_in_search_path(sentry_init, capture_events):
"""
Test that query source is relative to the path of the module it ran in
"""
sentry_init(
integrations=[AsyncPGIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
)
events = capture_events()
from asyncpg_helpers.helpers import execute_query_in_connection
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", {})
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) == "asyncpg_helpers.helpers"
assert data.get(SPANDATA.CODE_FILEPATH) == "asyncpg_helpers/helpers.py"
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "execute_query_in_connection"
@pytest.mark.asyncio
async def test_no_query_source_if_duration_too_short(sentry_init, capture_events):
sentry_init(
integrations=[AsyncPGIntegration()],
enable_tracing=True,
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=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()],
enable_tracing=True,
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
async def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[AsyncPGIntegration()],
traces_sample_rate=1.0,
)
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"
sentry-python-2.18.0/tests/integrations/aws_lambda/ 0000775 0000000 0000000 00000000000 14712146540 0022413 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/aws_lambda/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0024523 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("boto3")
sentry-python-2.18.0/tests/integrations/aws_lambda/client.py 0000664 0000000 0000000 00000030342 14712146540 0024245 0 ustar 00root root 0000000 0000000 import base64
import boto3
import glob
import hashlib
import os
import subprocess
import sys
import tempfile
from sentry_sdk.consts import VERSION as SDK_VERSION
from sentry_sdk.utils import get_git_revision
AWS_REGION_NAME = "us-east-1"
AWS_CREDENTIALS = {
"aws_access_key_id": os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"],
"aws_secret_access_key": os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"],
}
AWS_LAMBDA_EXECUTION_ROLE_NAME = "lambda-ex"
AWS_LAMBDA_EXECUTION_ROLE_ARN = None
def _install_dependencies(base_dir, subprocess_kwargs):
"""
Installs dependencies for AWS Lambda function
"""
setup_cfg = os.path.join(base_dir, "setup.cfg")
with open(setup_cfg, "w") as f:
f.write("[install]\nprefix=")
# Install requirements for Lambda Layer (these are more limited than the SDK requirements,
# because Lambda does not support the newest versions of some packages)
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"-r",
"requirements-aws-lambda-layer.txt",
"--target",
base_dir,
],
**subprocess_kwargs,
)
# Install requirements used for testing
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"mock==3.0.0",
"funcsigs",
"--target",
base_dir,
],
**subprocess_kwargs,
)
# Create a source distribution of the Sentry SDK (in parent directory of base_dir)
subprocess.check_call(
[
sys.executable,
"setup.py",
"sdist",
"--dist-dir",
os.path.dirname(base_dir),
],
**subprocess_kwargs,
)
# Install the created Sentry SDK source distribution into the target directory
# Do not install the dependencies of the SDK, because they where installed by requirements-aws-lambda-layer.txt above
source_distribution_archive = glob.glob(
"{}/*.tar.gz".format(os.path.dirname(base_dir))
)[0]
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
source_distribution_archive,
"--no-deps",
"--target",
base_dir,
],
**subprocess_kwargs,
)
def _create_lambda_function_zip(base_dir):
"""
Zips the given base_dir omitting Python cache files
"""
subprocess.run(
[
"zip",
"-q",
"-x",
"**/__pycache__/*",
"-r",
"lambda-function-package.zip",
"./",
],
cwd=base_dir,
check=True,
)
def _create_lambda_package(
base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs
):
"""
Creates deployable packages (as zip files) for AWS Lambda function
and optional the accompanying Sentry Lambda layer
"""
if initial_handler:
# If Initial handler value is provided i.e. it is not the default
# `test_lambda.test_handler`, then create another dir level so that our path is
# test_dir.test_lambda.test_handler
test_dir_path = os.path.join(base_dir, "test_dir")
python_init_file = os.path.join(test_dir_path, "__init__.py")
os.makedirs(test_dir_path)
with open(python_init_file, "w"):
# Create __init__ file to make it a python package
pass
test_lambda_py = os.path.join(base_dir, "test_dir", "test_lambda.py")
else:
test_lambda_py = os.path.join(base_dir, "test_lambda.py")
with open(test_lambda_py, "w") as f:
f.write(code)
if syntax_check:
# Check file for valid syntax first, and that the integration does not
# crash when not running in Lambda (but rather a local deployment tool
# such as chalice's)
subprocess.check_call([sys.executable, test_lambda_py])
if layer is None:
_install_dependencies(base_dir, subprocess_kwargs)
_create_lambda_function_zip(base_dir)
else:
_create_lambda_function_zip(base_dir)
# Create Lambda layer zip package
from scripts.build_aws_lambda_layer import build_packaged_zip
build_packaged_zip(
base_dir=base_dir,
make_dist=True,
out_zip_filename="lambda-layer-package.zip",
)
def _get_or_create_lambda_execution_role():
global AWS_LAMBDA_EXECUTION_ROLE_ARN
policy = """{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
"""
iam_client = boto3.client(
"iam",
region_name=AWS_REGION_NAME,
**AWS_CREDENTIALS,
)
try:
response = iam_client.get_role(RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME)
AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"]
except iam_client.exceptions.NoSuchEntityException:
# create role for lambda execution
response = iam_client.create_role(
RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME,
AssumeRolePolicyDocument=policy,
)
AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"]
# attach policy to role
iam_client.attach_role_policy(
RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME,
PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
)
def get_boto_client():
_get_or_create_lambda_execution_role()
return boto3.client(
"lambda",
region_name=AWS_REGION_NAME,
**AWS_CREDENTIALS,
)
def run_lambda_function(
client,
runtime,
code,
payload,
add_finalizer,
syntax_check=True,
timeout=30,
layer=None,
initial_handler=None,
subprocess_kwargs=(),
):
"""
Creates a Lambda function with the given code, and invokes it.
If the same code is run multiple times the function will NOT be
created anew each time but the existing function will be reused.
"""
subprocess_kwargs = dict(subprocess_kwargs)
# Making a unique function name depending on all the code that is run in it (function code plus SDK version)
# The name needs to be short so the generated event/envelope json blobs are small enough to be output
# in the log result of the Lambda function.
rev = get_git_revision() or SDK_VERSION
function_hash = hashlib.shake_256((code + rev).encode("utf-8")).hexdigest(6)
fn_name = "test_{}".format(function_hash)
full_fn_name = "{}_{}".format(
fn_name, runtime.replace(".", "").replace("python", "py")
)
function_exists_in_aws = True
try:
client.get_function(
FunctionName=full_fn_name,
)
print(
"Lambda function in AWS already existing, taking it (and do not create a local one)"
)
except client.exceptions.ResourceNotFoundException:
function_exists_in_aws = False
if not function_exists_in_aws:
tmp_base_dir = tempfile.gettempdir()
base_dir = os.path.join(tmp_base_dir, fn_name)
dir_already_existing = os.path.isdir(base_dir)
if dir_already_existing:
print("Local Lambda function directory already exists, skipping creation")
if not dir_already_existing:
os.mkdir(base_dir)
_create_lambda_package(
base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs
)
@add_finalizer
def clean_up():
# this closes the web socket so we don't get a
# ResourceWarning: unclosed
# warning on every test
# based on https://github.com/boto/botocore/pull/1810
# (if that's ever merged, this can just become client.close())
session = client._endpoint.http_session
managers = [session._manager] + list(session._proxy_managers.values())
for manager in managers:
manager.clear()
layers = []
environment = {}
handler = initial_handler or "test_lambda.test_handler"
if layer is not None:
with open(
os.path.join(base_dir, "lambda-layer-package.zip"), "rb"
) as lambda_layer_zip:
response = client.publish_layer_version(
LayerName="python-serverless-sdk-test",
Description="Created as part of testsuite for getsentry/sentry-python",
Content={"ZipFile": lambda_layer_zip.read()},
)
layers = [response["LayerVersionArn"]]
handler = (
"sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler"
)
environment = {
"Variables": {
"SENTRY_INITIAL_HANDLER": initial_handler
or "test_lambda.test_handler",
"SENTRY_DSN": "https://123abc@example.com/123",
"SENTRY_TRACES_SAMPLE_RATE": "1.0",
}
}
try:
with open(
os.path.join(base_dir, "lambda-function-package.zip"), "rb"
) as lambda_function_zip:
client.create_function(
Description="Created as part of testsuite for getsentry/sentry-python",
FunctionName=full_fn_name,
Runtime=runtime,
Timeout=timeout,
Role=AWS_LAMBDA_EXECUTION_ROLE_ARN,
Handler=handler,
Code={"ZipFile": lambda_function_zip.read()},
Environment=environment,
Layers=layers,
)
waiter = client.get_waiter("function_active_v2")
waiter.wait(FunctionName=full_fn_name)
except client.exceptions.ResourceConflictException:
print(
"Lambda function already exists, this is fine, we will just invoke it."
)
response = client.invoke(
FunctionName=full_fn_name,
InvocationType="RequestResponse",
LogType="Tail",
Payload=payload,
)
assert 200 <= response["StatusCode"] < 300, response
return response
# This is for inspecting new Python runtime environments in AWS Lambda
# If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands
# in that runtime in a Lambda function:
#
# pip3 install click
# python3 tests/integrations/aws_lambda/client.py --runtime=python4.0
#
_REPL_CODE = """
import os
def test_handler(event, context):
line = {line!r}
if line.startswith(">>> "):
exec(line[4:])
elif line.startswith("$ "):
os.system(line[2:])
else:
print("Start a line with $ or >>>")
return b""
"""
try:
import click
except ImportError:
pass
else:
@click.command()
@click.option(
"--runtime", required=True, help="name of the runtime to use, eg python3.11"
)
@click.option("--verbose", is_flag=True, default=False)
def repl(runtime, verbose):
"""
Launch a "REPL" against AWS Lambda to inspect their runtime.
"""
cleanup = []
client = get_boto_client()
print("Start a line with `$ ` to run shell commands, or `>>> ` to run Python")
while True:
line = input()
response = run_lambda_function(
client,
runtime,
_REPL_CODE.format(line=line),
b"",
cleanup.append,
subprocess_kwargs=(
{
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
}
if not verbose
else {}
),
)
for line in base64.b64decode(response["LogResult"]).splitlines():
print(line.decode("utf8"))
for f in cleanup:
f()
cleanup = []
if __name__ == "__main__":
repl()
sentry-python-2.18.0/tests/integrations/aws_lambda/test_aws.py 0000664 0000000 0000000 00000070664 14712146540 0024633 0 ustar 00root root 0000000 0000000 """
# AWS Lambda System Tests
This testsuite uses boto3 to upload actual Lambda functions to AWS Lambda and invoke them.
For running test locally you need to set these env vars:
(You can find the values in the Sentry password manager by searching for "AWS Lambda for Python SDK Tests").
export SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID="..."
export SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY="..."
You can use `scripts/aws-cleanup.sh` to delete all files generated by this test suite.
If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands
in that runtime in a Lambda function: (see the bottom of client.py for more information.)
pip3 install click
python3 tests/integrations/aws_lambda/client.py --runtime=python4.0
IMPORTANT:
During running of this test suite temporary folders will be created for compiling the Lambda functions.
This temporary folders will not be cleaned up. This is because in CI generated files have to be shared
between tests and thus the folders can not be deleted right after use.
If you run your tests locally, you need to clean up the temporary folders manually. The location of
the temporary folders is printed when running a test.
"""
import base64
import json
import re
from textwrap import dedent
import pytest
RUNTIMES_TO_TEST = [
"python3.8",
"python3.9",
"python3.10",
"python3.11",
"python3.12",
]
LAMBDA_PRELUDE = """
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration, get_lambda_bootstrap
import sentry_sdk
import json
import time
from sentry_sdk.transport import Transport
def truncate_data(data):
# AWS Lambda truncates the log output to 4kb, which is small enough to miss
# parts of even a single error-event/transaction-envelope pair if considered
# in full, so only grab the data we need.
cleaned_data = {}
if data.get("type") is not None:
cleaned_data["type"] = data["type"]
if data.get("contexts") is not None:
cleaned_data["contexts"] = {}
if data["contexts"].get("trace") is not None:
cleaned_data["contexts"]["trace"] = data["contexts"].get("trace")
if data.get("transaction") is not None:
cleaned_data["transaction"] = data.get("transaction")
if data.get("request") is not None:
cleaned_data["request"] = data.get("request")
if data.get("tags") is not None:
cleaned_data["tags"] = data.get("tags")
if data.get("exception") is not None:
cleaned_data["exception"] = data.get("exception")
for value in cleaned_data["exception"]["values"]:
for frame in value.get("stacktrace", {}).get("frames", []):
del frame["vars"]
del frame["pre_context"]
del frame["context_line"]
del frame["post_context"]
if data.get("extra") is not None:
cleaned_data["extra"] = {}
for key in data["extra"].keys():
if key == "lambda":
for lambda_key in data["extra"]["lambda"].keys():
if lambda_key in ["function_name"]:
cleaned_data["extra"].setdefault("lambda", {})[lambda_key] = data["extra"]["lambda"][lambda_key]
elif key == "cloudwatch logs":
for cloudwatch_key in data["extra"]["cloudwatch logs"].keys():
if cloudwatch_key in ["url", "log_group", "log_stream"]:
cleaned_data["extra"].setdefault("cloudwatch logs", {})[cloudwatch_key] = data["extra"]["cloudwatch logs"][cloudwatch_key]
if data.get("level") is not None:
cleaned_data["level"] = data.get("level")
if data.get("message") is not None:
cleaned_data["message"] = data.get("message")
if "contexts" not in cleaned_data:
raise Exception(json.dumps(data))
return cleaned_data
def event_processor(event):
return truncate_data(event)
def envelope_processor(envelope):
(item,) = envelope.items
item_json = json.loads(item.get_bytes())
return truncate_data(item_json)
class TestTransport(Transport):
def capture_envelope(self, envelope):
envelope_items = envelope_processor(envelope)
print("\\nENVELOPE: {}\\n".format(json.dumps(envelope_items)))
def init_sdk(timeout_warning=False, **extra_init_args):
sentry_sdk.init(
dsn="https://123abc@example.com/123",
transport=TestTransport,
integrations=[AwsLambdaIntegration(timeout_warning=timeout_warning)],
shutdown_timeout=10,
**extra_init_args
)
"""
@pytest.fixture
def lambda_client():
from tests.integrations.aws_lambda.client import get_boto_client
return get_boto_client()
@pytest.fixture(params=RUNTIMES_TO_TEST)
def lambda_runtime(request):
return request.param
@pytest.fixture
def run_lambda_function(request, lambda_client, lambda_runtime):
def inner(
code, payload, timeout=30, syntax_check=True, layer=None, initial_handler=None
):
from tests.integrations.aws_lambda.client import run_lambda_function
response = run_lambda_function(
client=lambda_client,
runtime=lambda_runtime,
code=code,
payload=payload,
add_finalizer=request.addfinalizer,
timeout=timeout,
syntax_check=syntax_check,
layer=layer,
initial_handler=initial_handler,
)
# Make sure the "ENVELOPE:" and "EVENT:" log entries are always starting a new line. (Sometimes they don't.)
response["LogResult"] = (
base64.b64decode(response["LogResult"])
.replace(b"EVENT:", b"\nEVENT:")
.replace(b"ENVELOPE:", b"\nENVELOPE:")
.splitlines()
)
response["Payload"] = json.loads(response["Payload"].read().decode("utf-8"))
del response["ResponseMetadata"]
envelope_items = []
for line in response["LogResult"]:
print("AWS:", line)
if line.startswith(b"ENVELOPE: "):
line = line[len(b"ENVELOPE: ") :]
envelope_items.append(json.loads(line.decode("utf-8")))
else:
continue
return envelope_items, response
return inner
def test_basic(run_lambda_function):
envelope_items, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
def test_handler(event, context):
raise Exception("Oh!")
"""
),
b'{"foo": "bar"}',
)
assert response["FunctionError"] == "Unhandled"
(event,) = envelope_items
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh!"
(frame1,) = exception["stacktrace"]["frames"]
assert frame1["filename"] == "test_lambda.py"
assert frame1["abs_path"] == "/var/task/test_lambda.py"
assert frame1["function"] == "test_handler"
assert frame1["in_app"] is True
assert exception["mechanism"]["type"] == "aws_lambda"
assert not exception["mechanism"]["handled"]
assert event["extra"]["lambda"]["function_name"].startswith("test_")
logs_url = event["extra"]["cloudwatch logs"]["url"]
assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region=")
assert not re.search("(=;|=$)", logs_url)
assert event["extra"]["cloudwatch logs"]["log_group"].startswith(
"/aws/lambda/test_"
)
log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$"
log_stream = event["extra"]["cloudwatch logs"]["log_stream"]
assert re.match(log_stream_re, log_stream)
def test_initialization_order(run_lambda_function):
"""Zappa lazily imports our code, so by the time we monkeypatch the handler
as seen by AWS already runs. At this point at least draining the queue
should work."""
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
def test_handler(event, context):
init_sdk()
sentry_sdk.capture_exception(Exception("Oh!"))
"""
),
b'{"foo": "bar"}',
)
(event,) = envelope_items
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh!"
def test_request_data(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
def test_handler(event, context):
sentry_sdk.capture_message("hi")
return "ok"
"""
),
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
}
""",
)
(event,) = envelope_items
assert 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",
}
@pytest.mark.xfail(
reason="Amazon changed something (2024-10-01) and on Python 3.9+ our SDK can not capture events in the init phase of the Lambda function anymore. We need to fix this somehow."
)
def test_init_error(run_lambda_function, lambda_runtime):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
func()
"""
),
b'{"foo": "bar"}',
syntax_check=False,
)
# We just take the last one, because it could be that in the output of the Lambda
# invocation there is still the envelope of the previous invocation of the function.
event = envelope_items[-1]
assert event["exception"]["values"][0]["value"] == "name 'func' is not defined"
def test_timeout_error(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(timeout_warning=True)
def test_handler(event, context):
time.sleep(10)
return 0
"""
),
b'{"foo": "bar"}',
timeout=2,
)
(event,) = envelope_items
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "ServerlessTimeoutWarning"
assert exception["value"] in (
"WARNING : Function is expected to get timed out. Configured timeout duration = 3 seconds.",
"WARNING : Function is expected to get timed out. Configured timeout duration = 2 seconds.",
)
assert exception["mechanism"]["type"] == "threading"
assert not exception["mechanism"]["handled"]
assert event["extra"]["lambda"]["function_name"].startswith("test_")
logs_url = event["extra"]["cloudwatch logs"]["url"]
assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region=")
assert not re.search("(=;|=$)", logs_url)
assert event["extra"]["cloudwatch logs"]["log_group"].startswith(
"/aws/lambda/test_"
)
log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$"
log_stream = event["extra"]["cloudwatch logs"]["log_stream"]
assert re.match(log_stream_re, log_stream)
def test_performance_no_error(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
return "test_string"
"""
),
b'{"foo": "bar"}',
)
(envelope,) = envelope_items
assert envelope["type"] == "transaction"
assert envelope["contexts"]["trace"]["op"] == "function.aws"
assert envelope["transaction"].startswith("test_")
assert envelope["transaction"] in envelope["request"]["url"]
def test_performance_error(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
raise Exception("Oh!")
"""
),
b'{"foo": "bar"}',
)
(
error_event,
transaction_event,
) = envelope_items
assert error_event["level"] == "error"
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh!"
assert transaction_event["type"] == "transaction"
assert transaction_event["contexts"]["trace"]["op"] == "function.aws"
assert transaction_event["transaction"].startswith("test_")
assert transaction_event["transaction"] in transaction_event["request"]["url"]
@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": "/path1",
"queryStringParameters": {
"done": "false"
},
"dog": "Maisey"
},
{
"headers": {
"Host": "x2.io",
"X-Forwarded-Proto": "http"
},
"httpMethod": "POST",
"path": "/path2",
"queryStringParameters": {
"done": "true"
},
"dog": "Charlie"
}
]
""",
True,
2,
),
(b"[]", False, 1),
],
)
def test_non_dict_event(
run_lambda_function,
aws_event,
has_request_data,
batch_size,
DictionaryContaining, # noqa:N803
):
envelope_items, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
raise Exception("Oh?")
"""
),
aws_event,
)
assert response["FunctionError"] == "Unhandled"
(
error_event,
transaction_event,
) = envelope_items
assert error_event["level"] == "error"
assert error_event["contexts"]["trace"]["op"] == "function.aws"
function_name = error_event["extra"]["lambda"]["function_name"]
assert function_name.startswith("test_")
assert error_event["transaction"] == function_name
exception = error_event["exception"]["values"][0]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh?"
assert exception["mechanism"]["type"] == "aws_lambda"
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"]["url"] == error_event["request"]["url"]
if has_request_data:
request_data = {
"headers": {"Host": "x1.io", "X-Forwarded-Proto": "https"},
"method": "GET",
"url": "https://x1.io/path1",
"query_string": {
"done": "false",
},
}
else:
request_data = {"url": "awslambda:///{}".format(function_name)}
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_traces_sampler_gets_correct_values_in_sampling_context(
run_lambda_function,
DictionaryContaining, # noqa: N803
ObjectDescribedBy, # noqa: N803
StringContaining, # noqa: N803
):
# TODO: This whole thing is a little hacky, specifically around the need to
# get `conftest.py` code into the AWS runtime, which is why there's both
# `inspect.getsource` and a copy of `_safe_is_equal` included directly in
# the code below. Ideas which have been discussed to fix this:
# - Include the test suite as a module installed in the package which is
# shot up to AWS
# - In client.py, copy `conftest.py` (or wherever the necessary code lives)
# from the test suite into the main SDK directory so it gets included as
# "part of the SDK"
# It's also worth noting why it's necessary to run the assertions in the AWS
# runtime rather than asserting on side effects the way we do with events
# and envelopes. The reasons are two-fold:
# - We're testing against the `LambdaContext` class, which only exists in
# the AWS runtime
# - If we were to transmit call args data they way we transmit event and
# envelope data (through JSON), we'd quickly run into the problem that all
# sorts of stuff isn't serializable by `json.dumps` out of the box, up to
# and including `datetime` objects (so anything with a timestamp is
# automatically out)
# Perhaps these challenges can be solved in a cleaner and more systematic
# way if we ever decide to refactor the entire AWS testing apparatus.
import inspect
_, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(inspect.getsource(StringContaining))
+ dedent(inspect.getsource(DictionaryContaining))
+ dedent(inspect.getsource(ObjectDescribedBy))
+ dedent(
"""
from unittest import mock
def _safe_is_equal(x, y):
# copied from conftest.py - see docstring and comments there
try:
is_equal = x.__eq__(y)
except AttributeError:
is_equal = NotImplemented
if is_equal == NotImplemented:
# using == smoothes out weird variations exposed by raw __eq__
return x == y
return is_equal
def test_handler(event, context):
# this runs after the transaction has started, which means we
# can make assertions about traces_sampler
try:
traces_sampler.assert_any_call(
DictionaryContaining(
{
"aws_event": DictionaryContaining({
"httpMethod": "GET",
"path": "/sit/stay/rollover",
"headers": {"Host": "x.io", "X-Forwarded-Proto": "http"},
}),
"aws_context": ObjectDescribedBy(
type=get_lambda_bootstrap().LambdaContext,
attrs={
'function_name': StringContaining("test_"),
'function_version': '$LATEST',
}
)
}
)
)
except AssertionError:
# catch the error and return it because the error itself will
# get swallowed by the SDK as an "internal exception"
return {"AssertionError raised": True,}
return {"AssertionError raised": False,}
traces_sampler = mock.Mock(return_value=True)
init_sdk(
traces_sampler=traces_sampler,
)
"""
),
b'{"httpMethod": "GET", "path": "/sit/stay/rollover", "headers": {"Host": "x.io", "X-Forwarded-Proto": "http"}}',
)
assert response["Payload"]["AssertionError raised"] is False
@pytest.mark.xfail(
reason="The limited log output we depend on is being clogged by a new warning"
)
def test_serverless_no_code_instrumentation(run_lambda_function):
"""
Test that ensures that just by adding a lambda layer containing the
python sdk, with no code changes sentry is able to capture errors
"""
for initial_handler in [
None,
"test_dir/test_lambda.test_handler",
"test_dir.test_lambda.test_handler",
]:
print("Testing Initial Handler ", initial_handler)
_, response = run_lambda_function(
dedent(
"""
import sentry_sdk
def test_handler(event, context):
current_client = sentry_sdk.get_client()
assert current_client.is_active()
assert len(current_client.options['integrations']) == 1
assert isinstance(current_client.options['integrations'][0],
sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration)
raise Exception("Oh!")
"""
),
b'{"foo": "bar"}',
layer=True,
initial_handler=initial_handler,
)
assert response["FunctionError"] == "Unhandled"
assert response["StatusCode"] == 200
assert response["Payload"]["errorType"] != "AssertionError"
assert response["Payload"]["errorType"] == "Exception"
assert response["Payload"]["errorMessage"] == "Oh!"
assert "sentry_handler" in response["LogResult"][3].decode("utf-8")
@pytest.mark.xfail(
reason="The limited log output we depend on is being clogged by a new warning"
)
def test_error_has_new_trace_context_performance_enabled(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
sentry_sdk.capture_message("hi")
raise Exception("Oh!")
"""
),
payload=b'{"foo": "bar"}',
)
(msg_event, error_event, transaction_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
def test_error_has_new_trace_context_performance_disabled(run_lambda_function):
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=None) # this is the default, just added for clarity
def test_handler(event, context):
sentry_sdk.capture_message("hi")
raise Exception("Oh!")
"""
),
payload=b'{"foo": "bar"}',
)
(msg_event, error_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
)
@pytest.mark.xfail(
reason="The limited log output we depend on is being clogged by a new warning"
)
def test_error_has_existing_trace_context_performance_enabled(run_lambda_function):
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,
}
}
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
sentry_sdk.capture_message("hi")
raise Exception("Oh!")
"""
),
payload=json.dumps(payload).encode(),
)
(msg_event, error_event, transaction_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
def test_error_has_existing_trace_context_performance_disabled(run_lambda_function):
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,
}
}
envelope_items, _ = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=None) # this is the default, just added for clarity
def test_handler(event, context):
sentry_sdk.capture_message("hi")
raise Exception("Oh!")
"""
),
payload=json.dumps(payload).encode(),
)
(msg_event, error_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
def test_basic_with_eventbridge_source(run_lambda_function):
envelope_items, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk()
def test_handler(event, context):
raise Exception("Oh!")
"""
),
b'[{"topic":"lps-ranges","partition":1,"offset":0,"timestamp":1701268939207,"timestampType":"CREATE_TIME","key":"REDACTED","value":"REDACTED","headers":[],"eventSourceArn":"REDACTED","bootstrapServers":"REDACTED","eventSource":"aws:kafka","eventSourceKey":"lps-ranges-1"}]',
)
assert response["FunctionError"] == "Unhandled"
(event,) = envelope_items
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "Oh!"
def test_span_origin(run_lambda_function):
envelope_items, response = run_lambda_function(
LAMBDA_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
def test_handler(event, context):
pass
"""
),
b'{"foo": "bar"}',
)
(event,) = envelope_items
assert event["contexts"]["trace"]["origin"] == "auto.function.aws_lambda"
sentry-python-2.18.0/tests/integrations/beam/ 0000775 0000000 0000000 00000000000 14712146540 0021225 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/beam/__init__.py 0000664 0000000 0000000 00000000062 14712146540 0023334 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("apache_beam")
sentry-python-2.18.0/tests/integrations/beam/test_beam.py 0000664 0000000 0000000 00000013556 14712146540 0023554 0 ustar 00root root 0000000 0000000 import pytest
import inspect
import dill
from sentry_sdk.integrations.beam import (
BeamIntegration,
_wrap_task_call,
_wrap_inspect_call,
)
from apache_beam.typehints.trivial_inference import instance_to_type
from apache_beam.typehints.decorators import getcallargs_forhints
from apache_beam.transforms.core import DoFn, ParDo, _DoFnParam, CallableWrapperDoFn
from apache_beam.runners.common import DoFnInvoker, DoFnContext
from apache_beam.utils.windowed_value import WindowedValue
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.18.0/tests/integrations/boto3/ 0000775 0000000 0000000 00000000000 14712146540 0021347 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/boto3/__init__.py 0000664 0000000 0000000 00000000346 14712146540 0023463 0 ustar 00root root 0000000 0000000 import pytest
import os
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.18.0/tests/integrations/boto3/aws_mock.py 0000664 0000000 0000000 00000001553 14712146540 0023530 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.18.0/tests/integrations/boto3/s3_list.xml 0000664 0000000 0000000 00000001545 14712146540 0023456 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.18.0/tests/integrations/boto3/test_s3.py 0000664 0000000 0000000 00000011255 14712146540 0023311 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="-",
)
def test_basic(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[Boto3Integration()])
events = capture_events()
s3 = session.resource("s3")
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
bucket = s3.Bucket("bucket")
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"
def test_streaming(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[Boto3Integration()])
events = capture_events()
s3 = session.resource("s3")
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
obj = s3.Bucket("bucket").Object("foo.pdf")
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"]
def test_streaming_close(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[Boto3Integration()])
events = capture_events()
s3 = session.resource("s3")
with sentry_sdk.start_transaction() as transaction, MockResponse(
s3.meta.client, 200, {}, b"hello"
):
obj = s3.Bucket("bucket").Object("foo.pdf")
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
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[Boto3Integration()])
events = capture_events()
s3 = session.resource("s3")
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")
):
bucket = s3.Bucket("bucket")
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"]
def test_span_origin(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[Boto3Integration()])
events = capture_events()
s3 = session.resource("s3")
with sentry_sdk.start_transaction(), MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
bucket = s3.Bucket("bucket")
_ = [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.18.0/tests/integrations/bottle/ 0000775 0000000 0000000 00000000000 14712146540 0021612 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/bottle/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023723 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("bottle")
sentry-python-2.18.0/tests/integrations/bottle/test_bottle.py 0000664 0000000 0000000 00000033051 14712146540 0024516 0 ustar 00root root 0000000 0000000 import json
import pytest
import logging
from io import BytesIO
from bottle import Bottle, debug as set_debug, abort, redirect, HTTPResponse
from sentry_sdk import capture_message
from sentry_sdk.integrations.bottle import BottleIntegration
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
from sentry_sdk.integrations.logging import LoggingIntegration
from werkzeug.test import Client
from werkzeug.wrappers import Response
import sentry_sdk.integrations.bottle as bottle_sentry
@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=[bottle_sentry.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=[
bottle_sentry.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=[bottle_sentry.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
def test_large_json_request(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
data = {"foo": {"bar": "a" * 2000}}
@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
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
@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=[bottle_sentry.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
def test_medium_formdata_request(sentry_init, capture_events, app, get_client):
sentry_init(integrations=[bottle_sentry.BottleIntegration()])
data = {"foo": "a" * 2000}
@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
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]) == 1024
@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=[bottle_sentry.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"]
def test_files_and_form(sentry_init, capture_events, app, get_client):
sentry_init(
integrations=[bottle_sentry.BottleIntegration()], max_request_body_size="always"
)
data = {"foo": "a" * 2000, "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
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]) == 1024
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=[bottle_sentry.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",
[
[bottle_sentry.BottleIntegration()],
[bottle_sentry.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():
try:
1 / 0
except Exception as e:
logger.exception(e)
raise e
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
assert len(events) == 1
def test_mount(app, capture_exceptions, capture_events, sentry_init, get_client):
sentry_init(integrations=[bottle_sentry.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=[bottle_sentry.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=[bottle_sentry.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=[bottle_sentry.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
def test_span_origin(
sentry_init,
get_client,
capture_events,
):
sentry_init(
integrations=[bottle_sentry.BottleIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = get_client()
client.get("/message")
(_, 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"
sentry-python-2.18.0/tests/integrations/celery/ 0000775 0000000 0000000 00000000000 14712146540 0021604 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/celery/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023715 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("celery")
sentry-python-2.18.0/tests/integrations/celery/integration_tests/ 0000775 0000000 0000000 00000000000 14712146540 0025351 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/celery/integration_tests/__init__.py 0000664 0000000 0000000 00000002733 14712146540 0027467 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.18.0/tests/integrations/celery/integration_tests/test_celery_beat_cron_monitoring.py0000664 0000000 0000000 00000010544 14712146540 0034532 0 ustar 00root root 0000000 0000000 import os
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 = "redis://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.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.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.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.18.0/tests/integrations/celery/test_celery.py 0000664 0000000 0000000 00000061757 14712146540 0024520 0 ustar 00root root 0000000 0000000 import threading
import kombu
from unittest import mock
import pytest
from celery import Celery, VERSION
from celery.bin import worker
import sentry_sdk
from sentry_sdk import start_transaction, get_current_span
from sentry_sdk.integrations.celery import (
CeleryIntegration,
_wrap_task_run,
)
from sentry_sdk.integrations.celery.beat import _get_headers
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
celery.conf.broker_url = "redis://127.0.0.1:6379"
celery.conf.result_backend = "redis://127.0.0.1:6379"
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
def test_simple_with_performance(capture_events, init_celery, celery_invocation):
celery = init_celery(traces_sample_rate=1.0)
events = capture_events()
@celery.task(name="dummy_task")
def dummy_task(x, y):
foo = 42 # noqa
return x / y
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"]
assert error_event["extra"]["celery-job"] == dict(
task_name="dummy_task", **expected_context
)
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "celery"
assert exception["stacktrace"]["frames"][0]["vars"]["foo"] == "42"
def test_simple_without_performance(capture_events, init_celery, celery_invocation):
celery = init_celery(traces_sample_rate=None)
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"]
assert error_event["extra"]["celery-job"] == dict(
task_name="dummy_task", **expected_context
)
(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("task_fails", [True, False], ids=["error", "success"])
def test_transaction_events(capture_events, init_celery, celery_invocation, task_fails):
celery = init_celery(traces_sample_rate=1.0)
@celery.task(name="dummy_task")
def dummy_task(x, y):
return x / y
# XXX: For some reason the first call does not get instrumented properly.
celery_invocation(dummy_task, 1, 1)
events = capture_events()
with start_transaction(name="submission") as transaction:
celery_invocation(dummy_task, 1, 0 if task_fails else 1)
if task_fails:
error_event = events.pop(0)
assert error_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
execution_event, submission_event = events
assert execution_event["transaction"] == "dummy_task"
assert 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_stackoverflows(celery):
"""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
"""
results = []
@celery.task(name="dummy_task")
def dummy_task():
sentry_sdk.get_isolation_scope().set_tag("foo", "bar")
results.append(42)
for _ in range(10000):
dummy_task.delay()
assert results == [42] * 10000
assert not sentry_sdk.get_isolation_scope()._tags
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
from newrelic.hooks.application_celery import (
instrument_celery_execute_trace,
)
import celery.app.trace as celery_trace_module
assert hasattr(celery_trace_module, "build_tracer")
instrument_celery_execute_trace(celery_trace_module)
except ImportError:
# newer newrelic versions
from newrelic.hooks.application_celery import instrument_celery_app_base
import celery.app as celery_app_module
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
def test_traces_sampler_gets_task_info_in_sampling_context(
init_celery, celery_invocation, DictionaryContaining # noqa:N803
):
traces_sampler = mock.Mock()
celery = init_celery(traces_sampler=traces_sampler)
@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)
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_rate=1.0",
"sentry-sampled=true",
"custom=value",
]
)
def test_sentry_propagate_traces_override(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"
)
@celery.task(name="dummy_task", bind=True)
def dummy_task(self, message):
trace_id = get_current_span().trace_id
return trace_id
with start_transaction() as transaction:
transaction_trace_id = transaction.trace_id
# should propagate trace
task_transaction_id = dummy_task.apply_async(
args=("some message",),
).get()
assert transaction_trace_id == task_transaction_id
# should NOT propagate trace (overrides `propagate_traces` parameter in integration constructor)
task_transaction_id = dummy_task.apply_async(
args=("another message",),
headers={"sentry-propagate-traces": False},
).get()
assert transaction_trace_id != task_transaction_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("routing_key", ("celery", "custom"))
@mock.patch("celery.app.task.Task.request")
def test_messaging_destination_name_default_exchange(
mock_request, routing_key, init_celery, capture_events
):
celery_app = init_celery(enable_tracing=True)
events = capture_events()
mock_request.delivery_info = {"routing_key": routing_key, "exchange": ""}
@celery_app.task()
def task(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.destination.name"] == routing_key
@mock.patch("celery.app.task.Task.request")
def test_messaging_destination_name_nondefault_exchange(
mock_request, init_celery, capture_events
):
"""
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(enable_tracing=True)
events = capture_events()
mock_request.delivery_info = {"routing_key": "celery", "exchange": "custom"}
@celery_app.task()
def task(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.destination.name" not in span["data"]
def test_messaging_id(init_celery, capture_events):
celery = init_celery(enable_tracing=True)
events = capture_events()
@celery.task
def example_task(): ...
example_task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.message.id" in span["data"]
def test_retry_count_zero(init_celery, capture_events):
celery = init_celery(enable_tracing=True)
events = capture_events()
@celery.task()
def task(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.message.retry.count"] == 0
@mock.patch("celery.app.task.Task.request")
def test_retry_count_nonzero(mock_request, init_celery, capture_events):
mock_request.retries = 3
celery = init_celery(enable_tracing=True)
events = capture_events()
@celery.task()
def task(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.message.retry.count"] == 3
@pytest.mark.parametrize("system", ("redis", "amqp"))
def test_messaging_system(system, init_celery, capture_events):
celery = init_celery(enable_tracing=True)
events = capture_events()
# 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(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert span["data"]["messaging.system"] == system
@pytest.mark.parametrize("system", ("amqp", "redis"))
def test_producer_span_data(system, monkeypatch, sentry_init, capture_events):
old_publish = kombu.messaging.Producer._publish
def publish(*args, **kwargs):
pass
monkeypatch.setattr(kombu.messaging.Producer, "_publish", publish)
sentry_init(integrations=[CeleryIntegration()], enable_tracing=True)
celery = Celery(__name__, broker=f"{system}://example.com") # noqa: E231
events = capture_events()
@celery.task()
def task(): ...
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)
def test_receive_latency(init_celery, capture_events):
celery = init_celery(traces_sample_rate=1.0)
events = capture_events()
@celery.task()
def task(): ...
task.apply_async()
(event,) = events
(span,) = event["spans"]
assert "messaging.message.receive.latency" in span["data"]
assert span["data"]["messaging.message.receive.latency"] > 0
def tests_span_origin_consumer(init_celery, capture_events):
celery = init_celery(enable_tracing=True)
celery.conf.broker_url = "redis://example.com" # noqa: E231
events = capture_events()
@celery.task()
def task(): ...
task.apply_async()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.queue.celery"
assert event["spans"][0]["origin"] == "auto.queue.celery"
def tests_span_origin_producer(monkeypatch, sentry_init, capture_events):
old_publish = kombu.messaging.Producer._publish
def publish(*args, **kwargs):
pass
monkeypatch.setattr(kombu.messaging.Producer, "_publish", publish)
sentry_init(integrations=[CeleryIntegration()], enable_tracing=True)
celery = Celery(__name__, broker="redis://example.com") # noqa: E231
events = capture_events()
@celery.task()
def task(): ...
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.forked
@mock.patch("celery.Celery.send_task")
def test_send_task_wrapped(
patched_send_task,
sentry_init,
capture_events,
reset_integrations,
):
sentry_init(integrations=[CeleryIntegration()], enable_tracing=True)
celery = Celery(__name__, broker="redis://example.com") # noqa: E231
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"]
)
(event,) = events # We should have exactly one event (the transaction)
assert event["type"] == "transaction"
assert event["transaction"] == "custom_transaction"
(span,) = event["spans"] # We should have exactly one span
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.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.18.0/tests/integrations/celery/test_celery_beat_crons.py 0000664 0000000 0000000 00000037256 14712146540 0026714 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_maybe_due,
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_maybe_due = MagicMock()
fake_redbeat_scheduler = MagicMock()
fake_redbeat_scheduler.maybe_due = fake_maybe_due
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.maybe_due()
_patch_redbeat_maybe_due()
# Mimic Celery RedBeat calling a task from the RedBeat schedule
RedBeatScheduler.maybe_due(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_maybe_due.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_maybe_due.call_count == 1
assert _get_monitor_config.call_count == 1
sentry-python-2.18.0/tests/integrations/celery/test_update_celery_task_headers.py 0000664 0000000 0000000 00000021447 14712146540 0030567 0 ustar 00root root 0000000 0000000 from copy import copy
import itertools
import pytest
from unittest import mock
from sentry_sdk.integrations.celery import _update_celery_task_headers
import sentry_sdk
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(enable_tracing=True)
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(enable_tracing=True)
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.18.0/tests/integrations/chalice/ 0000775 0000000 0000000 00000000000 14712146540 0021711 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/chalice/__init__.py 0000664 0000000 0000000 00000000056 14712146540 0024023 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("chalice")
sentry-python-2.18.0/tests/integrations/chalice/test_chalice.py 0000664 0000000 0000000 00000010445 14712146540 0024716 0 ustar 00root root 0000000 0000000 import pytest
import time
from chalice import Chalice, BadRequestError
from chalice.local import LambdaContext, LocalGateway
from sentry_sdk import capture_message
from sentry_sdk.integrations.chalice import CHALICE_VERSION, ChaliceIntegration
from sentry_sdk.utils import parse_version
from pytest_chalice.handlers import RequestHandler
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
return LambdaContext(
function_name=self._config.function_name,
memory_size=self._config.lambda_memory_size,
max_runtime_ms=timeout,
)
@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 = 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, 28),
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, 28),
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}
sentry-python-2.18.0/tests/integrations/clickhouse_driver/ 0000775 0000000 0000000 00000000000 14712146540 0024025 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/clickhouse_driver/__init__.py 0000664 0000000 0000000 00000000070 14712146540 0026133 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("clickhouse_driver")
sentry-python-2.18.0/tests/integrations/clickhouse_driver/test_clickhouse_driver.py 0000664 0000000 0000000 00000073406 14712146540 0031154 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
```
"""
import clickhouse_driver
from clickhouse_driver import Client, connect
from sentry_sdk import start_transaction, capture_message
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.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.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.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.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.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_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
def test_clickhouse_client_spans(
sentry_init, capture_events, capture_envelopes
) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
)
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,
},
"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,
},
"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,
},
"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,
},
"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,
},
"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_client_spans_with_pii(
sentry_init, capture_events, capture_envelopes
) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
send_default_pii=True,
)
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.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.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.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.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.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
def test_clickhouse_dbapi_spans(sentry_init, capture_events, capture_envelopes) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
)
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,
},
"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,
},
"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,
},
"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,
},
"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,
},
"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_spans_with_pii(
sentry_init, capture_events, capture_envelopes
) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
send_default_pii=True,
)
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
def test_span_origin(sentry_init, capture_events, capture_envelopes) -> None:
sentry_init(
integrations=[ClickhouseDriverIntegration()],
traces_sample_rate=1.0,
)
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.18.0/tests/integrations/cloud_resource_context/ 0000775 0000000 0000000 00000000000 14712146540 0025102 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/cloud_resource_context/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0027201 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/cloud_resource_context/test_cloud_resource_context.py 0000664 0000000 0000000 00000031526 14712146540 0033303 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.18.0/tests/integrations/cohere/ 0000775 0000000 0000000 00000000000 14712146540 0021566 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/cohere/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023677 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("cohere")
sentry-python-2.18.0/tests/integrations/cohere/test_cohere.py 0000664 0000000 0000000 00000021133 14712146540 0024444 0 ustar 00root root 0000000 0000000 import json
import httpx
import pytest
from cohere import Client, ChatMessage
from sentry_sdk import start_transaction
from sentry_sdk.integrations.cohere import CohereIntegration
from unittest import mock # python 3.3 and above
from httpx import Client as HTTPXClient
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_nonstreaming_chat(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
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"):
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"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.cohere"
assert span["data"]["ai.model_id"] == "some-model"
if send_default_pii and include_prompts:
assert "some context" in span["data"]["ai.input_messages"][0]["content"]
assert "hello" in span["data"]["ai.input_messages"][1]["content"]
assert "the model response" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
# noinspection PyTypeChecker
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_streaming_chat(sentry_init, capture_events, send_default_pii, include_prompts):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
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,
}
},
},
}
),
]
),
)
)
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"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.cohere"
assert span["data"]["ai.model_id"] == "some-model"
if send_default_pii and include_prompts:
assert "some context" in span["data"]["ai.input_messages"][0]["content"]
assert "hello" in span["data"]["ai.input_messages"][1]["content"]
assert "the model response" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 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,) = events
assert event["level"] == "error"
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_embed(sentry_init, capture_events, send_default_pii, include_prompts):
sentry_init(
integrations=[CohereIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
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"):
response = client.embed(texts=["hello"], model="text-embedding-3-large")
assert len(response.embeddings[0]) == 3
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.embeddings.create.cohere"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]
else:
assert "ai.input_messages" not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 10
assert span["measurements"]["ai_total_tokens_used"]["value"] == 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.18.0/tests/integrations/conftest.py 0000664 0000000 0000000 00000002206 14712146540 0022520 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
sentry-python-2.18.0/tests/integrations/django/ 0000775 0000000 0000000 00000000000 14712146540 0021563 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/__init__.py 0000664 0000000 0000000 00000000434 14712146540 0023675 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.18.0/tests/integrations/django/asgi/ 0000775 0000000 0000000 00000000000 14712146540 0022506 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/asgi/__init__.py 0000664 0000000 0000000 00000000057 14712146540 0024621 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("channels")
sentry-python-2.18.0/tests/integrations/django/asgi/image.png 0000664 0000000 0000000 00000000464 14712146540 0024302 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.18.0/tests/integrations/django/asgi/test_asgi.py 0000664 0000000 0000000 00000047775 14712146540 0025066 0 ustar 00root root 0000000 0000000 import base64
import sys
import json
import inspect
import asyncio
import os
from unittest import mock
import django
import pytest
from channels.testing import HttpCommunicator
from sentry_sdk import capture_message
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.mark.parametrize("application", APPS)
@pytest.mark.asyncio
@pytest.mark.forked
async def test_basic(sentry_init, capture_events, application):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
events = capture_events()
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.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_async_views(sentry_init, capture_events, application):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
events = capture_events()
comm = HttpCommunicator(application, "GET", "/async_message")
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.asyncio
@pytest.mark.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_active_thread_id(
sentry_init, capture_envelopes, teardown_profiling, endpoint, application
):
with mock.patch(
"sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0
):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
envelopes = capture_envelopes()
comm = HttpCommunicator(application, "GET", endpoint)
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.forked
@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):
import asyncio
import time
settings.MIDDLEWARE = []
asgi_application.load_middleware(is_async=True)
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
comm = HttpCommunicator(
asgi_application, "GET", "/my_async_view"
) # sleeps for 1 second
comm2 = HttpCommunicator(
asgi_application, "GET", "/my_async_view"
) # sleeps for 1 second
loop = asyncio.get_event_loop()
start = time.time()
r1 = loop.create_task(comm.get_response(timeout=5))
r2 = loop.create_task(comm2.get_response(timeout=5))
(resp1, resp2), _ = await asyncio.wait({r1, r2})
end = time.time()
assert resp1.result()["status"] == 200
assert resp2.result()["status"] == 200
assert (
end - start < 2
) # it takes less than 2 seconds so it was ececuting concurrently
@pytest.mark.asyncio
@pytest.mark.forked
@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
):
import asyncio
import time
settings.MIDDLEWARE = [
"tests.integrations.django.myapp.middleware.simple_middleware"
]
asgi_application.load_middleware(is_async=True)
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
)
comm = HttpCommunicator(
asgi_application, "GET", "/my_async_view"
) # sleeps for 1 second
comm2 = HttpCommunicator(
asgi_application, "GET", "/my_async_view"
) # sleeps for 1 second
loop = asyncio.get_event_loop()
start = time.time()
r1 = loop.create_task(comm.get_response(timeout=5))
r2 = loop.create_task(comm2.get_response(timeout=5))
(resp1, resp2), _ = await asyncio.wait({r1, r2})
end = time.time()
assert resp1.result()["status"] == 200
assert resp2.result()["status"] == 200
assert (
end - start < 2
) # it takes less than 2 seconds so it was ececuting concurrently
@pytest.mark.asyncio
@pytest.mark.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_async_middleware_spans(
sentry_init, render_span_tree, capture_events, settings
):
settings.MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"tests.integrations.django.myapp.settings.TestMiddleware",
]
asgi_application.load_middleware(is_async=True)
sentry_init(
integrations=[DjangoIntegration(middleware_spans=True)],
traces_sample_rate=1.0,
_experiments={"record_sql_params": True},
)
events = capture_events()
comm = HttpCommunicator(asgi_application, "GET", "/simple_async_view")
response = await comm.get_response()
await comm.wait()
assert response["status"] == 200
(transaction,) = events
assert (
render_span_tree(transaction)
== """\
- op="http.server": description=null
- op="event.django": description="django.db.reset_queries"
- op="event.django": description="django.db.close_old_connections"
- 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"
- op="event.django": description="django.db.close_old_connections"
- op="event.django": description="django.core.cache.close_caches"
- op="event.django": description="django.core.handlers.base.reset_urlconf\""""
)
@pytest.mark.asyncio
@pytest.mark.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_has_trace_if_performance_enabled(sentry_init, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg")
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.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_has_trace_if_performance_disabled(sentry_init, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
)
events = capture_events()
comm = HttpCommunicator(asgi_application, "GET", "/view-exc-with-msg")
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.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
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())],
)
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.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
)
events = capture_events()
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())],
)
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.forked
@pytest.mark.skipif(
django.VERSION < (3, 1), reason="async views have been introduced in Django 3.1"
)
async def test_asgi_request_body(
sentry_init,
capture_envelopes,
application,
send_default_pii,
method,
headers,
url_name,
body,
expected_data,
):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=send_default_pii,
)
envelopes = capture_envelopes()
comm = HttpCommunicator(
application,
method=method,
headers=headers,
path=reverse(url_name),
body=body,
)
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"
),
)
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
async def test_async_view(sentry_init, capture_events, application):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
comm = HttpCommunicator(application, "GET", "/simple_async_view")
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
async def test_transaction_http_method_default(
sentry_init, capture_events, application
):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
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
async def test_transaction_http_method_custom(sentry_init, capture_events, application):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
)
],
traces_sample_rate=1.0,
)
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.18.0/tests/integrations/django/django_helpers/ 0000775 0000000 0000000 00000000000 14712146540 0024547 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/django_helpers/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0026646 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/django_helpers/views.py 0000664 0000000 0000000 00000000456 14712146540 0026263 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.18.0/tests/integrations/django/myapp/ 0000775 0000000 0000000 00000000000 14712146540 0022711 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0025010 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/asgi.py 0000664 0000000 0000000 00000000747 14712146540 0024216 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.18.0/tests/integrations/django/myapp/custom_urls.py 0000664 0000000 0000000 00000001724 14712146540 0025646 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.18.0/tests/integrations/django/myapp/manage.py 0000664 0000000 0000000 00000000434 14712146540 0024514 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.18.0/tests/integrations/django/myapp/management/ 0000775 0000000 0000000 00000000000 14712146540 0025025 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/management/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0027124 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/management/commands/ 0000775 0000000 0000000 00000000000 14712146540 0026626 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/management/commands/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0030725 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/management/commands/mycrash.py 0000664 0000000 0000000 00000000273 14712146540 0030650 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.18.0/tests/integrations/django/myapp/middleware.py 0000664 0000000 0000000 00000001420 14712146540 0025375 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.18.0/tests/integrations/django/myapp/routing.py 0000664 0000000 0000000 00000000756 14712146540 0024762 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.18.0/tests/integrations/django/myapp/settings.py 0000664 0000000 0000000 00000011675 14712146540 0025135 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.18.0/tests/integrations/django/myapp/signals.py 0000664 0000000 0000000 00000000567 14712146540 0024733 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.18.0/tests/integrations/django/myapp/templates/ 0000775 0000000 0000000 00000000000 14712146540 0024707 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/django/myapp/templates/error.html 0000664 0000000 0000000 00000000113 14712146540 0026721 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.18.0/tests/integrations/django/myapp/templates/trace_meta.html 0000664 0000000 0000000 00000000030 14712146540 0027672 0 ustar 00root root 0000000 0000000 {{ sentry_trace_meta }}
sentry-python-2.18.0/tests/integrations/django/myapp/templates/user_name.html 0000664 0000000 0000000 00000000043 14712146540 0027550 0 ustar 00root root 0000000 0000000 {{ request.user }}: {{ user_age }}
sentry-python-2.18.0/tests/integrations/django/myapp/urls.py 0000664 0000000 0000000 00000011174 14712146540 0024254 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
from django_helpers import views as helper_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("postgres-select", views.postgres_select, name="postgres_select"),
path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"),
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.18.0/tests/integrations/django/myapp/views.py 0000664 0000000 0000000 00000014647 14712146540 0024434 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.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 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 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.18.0/tests/integrations/django/myapp/wsgi.py 0000664 0000000 0000000 00000000643 14712146540 0024237 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.18.0/tests/integrations/django/test_basic.py 0000664 0000000 0000000 00000122213 14712146540 0024256 0 ustar 00root root 0000000 0000000 import inspect
import json
import os
import re
import sys
import pytest
from functools import partial
from unittest.mock import patch
from werkzeug.test import Client
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 OperationalError, ProgrammingError, DataError
from django.http.request import RawPostDataException
from django.utils.functional import SimpleLazyObject
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
import sentry_sdk
from sentry_sdk._compat import PY310
from sentry_sdk import capture_message, capture_exception
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.wsgi import application
from tests.integrations.django.myapp.signals import myapp_custom_signal_silenced
from tests.integrations.django.utils import pytest_mark_django_db_decorator
DJANGO_VERSION = DJANGO_VERSION[:2]
@pytest.fixture
def client():
return Client(application)
def test_view_exceptions(sentry_init, client, capture_exceptions, capture_events):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
exceptions = capture_exceptions()
events = capture_events()
client.get(reverse("view_exc"))
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "django"
def test_ensures_x_forwarded_header_is_honored_in_sdk_when_enabled_in_django(
sentry_init, client, capture_exceptions, capture_events, settings
):
"""
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)
exceptions = capture_exceptions()
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"
def test_ensures_x_forwarded_header_is_not_honored_when_unenabled_in_django(
sentry_init, client, capture_exceptions, capture_events
):
"""
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)
exceptions = capture_exceptions()
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)
def test_request_captured(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
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",
}
def test_transaction_with_class_view(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration(transaction_style="function_name")],
send_default_pii=True,
)
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"
def test_has_trace_if_performance_enabled(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
)
events = capture_events()
client.head(reverse("view_exc_with_msg"))
(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"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
def test_has_trace_if_performance_disabled(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
)
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"]
)
def test_trace_from_headers_if_performance_enabled(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
traces_sample_rate=1.0,
)
events = capture_events()
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
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
def test_trace_from_headers_if_performance_disabled(
sentry_init, client, capture_events
):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=("HEAD",),
)
],
)
events = capture_events()
trace_id = "582b43a4192642f0b136d5159a501701"
sentry_trace_header = "{}-{}-{}".format(trace_id, "6e8f22c393e68f19", 1)
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()
def test_user_captured(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
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_queryset_repr(sentry_init, capture_events):
sentry_init(integrations=[DjangoIntegration()])
events = capture_events()
User.objects.create_user("john", "lennon@thebeatles.com", "johnpassword")
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()]])
def test_template_exception(
sentry_init, client, capture_events, with_executing_integration
):
sentry_init(integrations=[DjangoIntegration()] + with_executing_integration)
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"}],
],
)
def test_rest_framework_basic(
sentry_init, client, capture_events, capture_exceptions, ct, body, route
):
pytest.importorskip("rest_framework")
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
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"]
)
def test_does_not_capture_403(sentry_init, client, capture_events, endpoint):
if endpoint == "rest_permission_denied_exc":
pytest.importorskip("rest_framework")
sentry_init(integrations=[DjangoIntegration()])
events = capture_events()
_, status, _ = unpack_werkzeug_response(client.get(reverse(endpoint)))
assert status.lower() == "403 forbidden"
assert not events
def test_render_spans(sentry_init, client, capture_events, render_span_tree):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
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)
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"\
"""
def test_middleware_spans(sentry_init, client, capture_events, render_span_tree):
sentry_init(
integrations=[
DjangoIntegration(signals_spans=False),
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert render_span_tree(transaction) == EXPECTED_MIDDLEWARE_SPANS
def test_middleware_spans_disabled(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False, signals_spans=False),
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert not len(transaction["spans"])
EXPECTED_SIGNALS_SPANS = """\
- op="http.server": description=null
- op="event.django": description="django.db.reset_queries"
- op="event.django": description="django.db.close_old_connections"\
"""
def test_signals_spans(sentry_init, client, capture_events, render_span_tree):
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False),
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert render_span_tree(transaction) == EXPECTED_SIGNALS_SPANS
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"
def test_signals_spans_disabled(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(middleware_spans=False, signals_spans=False),
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("message"))
message, transaction = events
assert message["message"] == "hi"
assert not transaction["spans"]
EXPECTED_SIGNALS_SPANS_FILTERED = """\
- 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"\
"""
def test_signals_spans_filtering(sentry_init, client, capture_events, render_span_tree):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=False,
signals_denylist=[
myapp_custom_signal_silenced,
],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("send_myapp_custom_signal"))
(transaction,) = events
assert render_span_tree(transaction) == EXPECTED_SIGNALS_SPANS_FILTERED
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")
def test_custom_urlconf_middleware(
settings, sentry_init, client, capture_events, render_span_tree
):
"""
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()], traces_sample_rate=1.0)
events = capture_events()
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"
assert "custom_urlconf_middleware" in render_span_tree(event)
_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"
assert "custom_urlconf_middleware" in render_span_tree(transaction_event)
settings.MIDDLEWARE.pop(0)
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")
def test_span_origin(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=True,
signals_spans=True,
cache_spans=True,
)
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("view_with_signal"))
(transaction,) = events
assert transaction["contexts"]["trace"]["origin"] == "auto.http.django"
signal_span_found = False
for span in transaction["spans"]:
assert span["origin"] == "auto.http.django"
if span["op"] == "event.django":
signal_span_found = True
assert signal_span_found
def test_transaction_http_method_default(sentry_init, client, capture_events):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
(event,) = events
assert len(events) == 1
assert event["request"]["method"] == "GET"
def test_transaction_http_method_custom(sentry_init, client, capture_events):
sentry_init(
integrations=[
DjangoIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
)
],
traces_sample_rate=1.0,
)
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.18.0/tests/integrations/django/test_cache_module.py 0000664 0000000 0000000 00000050206 14712146540 0025607 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")
def test_cache_spans_disabled_middleware(
sentry_init, client, capture_events, use_django_caching_with_middlewares
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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")
def test_cache_spans_disabled_decorator(
sentry_init, client, capture_events, use_django_caching
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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")
def test_cache_spans_disabled_templatetag(
sentry_init, client, capture_events, use_django_caching
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=False,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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")
def test_cache_spans_middleware(
sentry_init, client, capture_events, use_django_caching_with_middlewares
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
client.application.load_middleware()
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 not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
# 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")
def test_cache_spans_decorator(sentry_init, client, capture_events, use_django_caching):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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")
def test_cache_spans_templatetag(
sentry_init, client, capture_events, use_django_caching
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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_description",
[
(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.
],
)
def test_cache_spans_get_span_description(
method_name, args, kwargs, expected_description
):
assert _get_span_description(method_name, args, kwargs) == expected_description
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_cache_spans_location_with_port(
sentry_init, client, capture_events, use_django_caching_with_port
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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()
def test_cache_spans_location_without_port(
sentry_init, client, capture_events, use_django_caching_without_port
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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()
def test_cache_spans_location_with_cluster(
sentry_init, client, capture_events, use_django_caching_with_cluster
):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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()
def test_cache_spans_item_size(sentry_init, client, capture_events, use_django_caching):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
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 not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
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()
def test_cache_spans_get_many(sentry_init, capture_events, use_django_caching):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
events = capture_events()
id = os.getpid()
from django.core.cache import cache
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 transaction["spans"][1]["op"] == "cache.get"
assert transaction["spans"][1]["description"] == f"S{id}"
assert transaction["spans"][2]["op"] == "cache.get"
assert transaction["spans"][2]["description"] == f"S{id+1}"
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"][5]["op"] == "cache.get"
assert transaction["spans"][5]["description"] == f"S{id}"
assert transaction["spans"][6]["op"] == "cache.get"
assert transaction["spans"][6]["description"] == f"S{id+1}"
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_cache_spans_set_many(sentry_init, capture_events, use_django_caching):
sentry_init(
integrations=[
DjangoIntegration(
cache_spans=True,
middleware_spans=False,
signals_spans=False,
)
],
traces_sample_rate=1.0,
)
events = capture_events()
id = os.getpid()
from django.core.cache import cache
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")
def test_span_origin_cache(sentry_init, client, capture_events, use_django_caching):
sentry_init(
integrations=[
DjangoIntegration(
middleware_spans=True,
signals_spans=True,
cache_spans=True,
)
],
traces_sample_rate=1.0,
)
events = capture_events()
client.get(reverse("cached_view"))
(transaction,) = events
assert transaction["contexts"]["trace"]["origin"] == "auto.http.django"
cache_span_found = False
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.18.0/tests/integrations/django/test_data_scrubbing.py 0000664 0000000 0000000 00000004610 14712146540 0026144 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()
def test_scrub_django_session_cookies_removed(
sentry_init,
client,
capture_events,
):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=False)
events = capture_events()
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,) = events
assert "cookies" not in event["request"]
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_scrub_django_session_cookies_filtered(
sentry_init,
client,
capture_events,
):
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
events = capture_events()
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,) = events
assert event["request"]["cookies"] == {
"sessionid": "[Filtered]",
"csrftoken": "[Filtered]",
"foo": "bar",
}
@pytest.mark.forked
@pytest_mark_django_db_decorator()
def test_scrub_django_custom_session_cookies_filtered(
sentry_init,
client,
capture_events,
settings,
):
settings.SESSION_COOKIE_NAME = "my_sess"
settings.CSRF_COOKIE_NAME = "csrf_secret"
sentry_init(integrations=[DjangoIntegration()], send_default_pii=True)
events = capture_events()
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,) = events
assert event["request"]["cookies"] == {
"my_sess": "[Filtered]",
"csrf_secret": "[Filtered]",
"foo": "bar",
}
sentry-python-2.18.0/tests/integrations/django/test_db_query_data.py 0000664 0000000 0000000 00000040734 14712146540 0026007 0 ustar 00root root 0000000 0000000 import os
import pytest
from datetime import datetime
from unittest import mock
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
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.utils import pytest_mark_django_db_decorator
from tests.integrations.django.myapp.wsgi import application
@pytest.fixture
def client():
return Client(application)
@pytest.mark.forked
@pytest_mark_django_db_decorator(transaction=True)
def test_query_source_disabled(sentry_init, client, capture_events):
sentry_options = {
"integrations": [DjangoIntegration()],
"send_default_pii": True,
"traces_sample_rate": 1.0,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
}
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
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])
def test_query_source_enabled(
sentry_init, client, capture_events, enable_db_query_source
):
sentry_options = {
"integrations": [DjangoIntegration()],
"send_default_pii": True,
"traces_sample_rate": 1.0,
"db_query_source_threshold_ms": 0,
}
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
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)
def test_query_source(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
)
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
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)
def test_query_source_with_module_in_search_path(sentry_init, client, capture_events):
"""
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,
)
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
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)
def test_query_source_with_in_app_exclude(sentry_init, client, capture_events):
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"],
)
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
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)
def test_query_source_with_in_app_include(sentry_init, client, capture_events):
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"],
)
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
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)
def test_no_query_source_if_duration_too_short(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
)
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
events = capture_events()
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
self.span = span
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=99999)
def __enter__(self):
return self.span
def __exit__(self, type, value, traceback):
pass
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)
def test_query_source_if_duration_over_threshold(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
send_default_pii=True,
traces_sample_rate=1.0,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
)
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
events = capture_events()
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
self.span = span
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=101000)
def __enter__(self):
return self.span
def __exit__(self, type, value, traceback):
pass
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)
def test_db_span_origin_execute(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
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
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)
def test_db_span_origin_executemany(sentry_init, client, capture_events):
sentry_init(
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
if "postgres" not in connections:
pytest.skip("postgres tests disabled")
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.18.0/tests/integrations/django/test_middleware.py 0000664 0000000 0000000 00000002066 14712146540 0025315 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):
# type: (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.18.0/tests/integrations/django/test_transactions.py 0000664 0000000 0000000 00000011636 14712146540 0025713 0 ustar 00root root 0000000 0000000 from unittest import mock
import pytest
import django
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.urls import path, re_path
from django.urls.converters import PathConverter
from django.conf.urls import include
else:
from django.conf.urls import url as re_path, include
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.18.0/tests/integrations/django/utils.py 0000664 0000000 0000000 00000001331 14712146540 0023273 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.18.0/tests/integrations/dramatiq/ 0000775 0000000 0000000 00000000000 14712146540 0022123 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/dramatiq/__init__.py 0000664 0000000 0000000 00000000057 14712146540 0024236 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("dramatiq")
sentry-python-2.18.0/tests/integrations/dramatiq/test_dramatiq.py 0000664 0000000 0000000 00000013457 14712146540 0025350 0 ustar 00root root 0000000 0000000 import pytest
import uuid
import dramatiq
from dramatiq.brokers.stub import StubBroker
import sentry_sdk
from sentry_sdk.integrations.dramatiq import DramatiqIntegration
@pytest.fixture
def broker(sentry_init):
sentry_init(integrations=[DramatiqIntegration()])
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()
def test_that_a_single_error_is_captured(broker, worker, capture_events):
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)
broker.join(dummy_actor.queue_name)
worker.join()
(event,) = events
exception = event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
def test_that_actor_name_is_set_as_transaction(broker, worker, capture_events):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send(1, 0)
broker.join(dummy_actor.queue_name)
worker.join()
(event,) = events
assert event["transaction"] == "dummy_actor"
def test_that_dramatiq_message_id_is_set_as_extra(broker, worker, capture_events):
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)
broker.join(dummy_actor.queue_name)
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)
def test_that_local_variables_are_captured(broker, worker, capture_events):
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)
broker.join(dummy_actor.queue_name)
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"
def test_that_sub_actor_errors_are_captured(broker, worker, capture_events):
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)
broker.join(dummy_actor.queue_name)
worker.join()
(event,) = events
assert event["transaction"] == "sub_actor"
exception = event["exception"]["values"][0]
assert exception["type"] == "ZeroDivisionError"
def test_that_multiple_errors_are_captured(broker, worker, capture_events):
events = capture_events()
@dramatiq.actor(max_retries=0)
def dummy_actor(x, y):
return x / y
dummy_actor.send(1, 0)
broker.join(dummy_actor.queue_name)
worker.join()
dummy_actor.send(1, None)
broker.join(dummy_actor.queue_name)
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"
def test_that_message_data_is_added_as_request(broker, worker, capture_events):
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,
)
broker.join(dummy_actor.queue_name)
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)
def test_that_expected_exceptions_are_not_captured(broker, worker, capture_events):
events = capture_events()
class ExpectedException(Exception):
pass
@dramatiq.actor(max_retries=0, throws=ExpectedException)
def dummy_actor():
raise ExpectedException
dummy_actor.send()
broker.join(dummy_actor.queue_name)
worker.join()
assert events == []
def test_that_retry_exceptions_are_not_captured(broker, worker, capture_events):
events = capture_events()
@dramatiq.actor(max_retries=2)
def dummy_actor():
raise dramatiq.errors.Retry("Retrying", delay=100)
dummy_actor.send()
broker.join(dummy_actor.queue_name)
worker.join()
assert events == []
sentry-python-2.18.0/tests/integrations/excepthook/ 0000775 0000000 0000000 00000000000 14712146540 0022472 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/excepthook/test_excepthook.py 0000664 0000000 0000000 00000004604 14712146540 0026260 0 ustar 00root root 0000000 0000000 import pytest
import sys
import subprocess
from textwrap import dedent
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
print(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
print(output)
assert b"ZeroDivisionError" in output
assert b"LOL" in output
assert b"capture_envelope was called" in output
sentry-python-2.18.0/tests/integrations/falcon/ 0000775 0000000 0000000 00000000000 14712146540 0021563 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/falcon/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023674 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("falcon")
sentry-python-2.18.0/tests/integrations/falcon/test_falcon.py 0000664 0000000 0000000 00000030245 14712146540 0024442 0 ustar 00root root 0000000 0000000 import logging
import pytest
import falcon
import falcon.testing
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
def test_has_context(sentry_init, capture_events, make_client):
sentry_init(integrations=[FalconIntegration()])
events = capture_events()
client = make_client()
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"),
],
)
def test_transaction_style(
sentry_init,
make_client,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
integration = FalconIntegration(transaction_style=transaction_style)
sentry_init(integrations=[integration])
events = capture_events()
client = make_client()
response = client.simulate_get(url)
assert response.status == falcon.HTTP_200
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
def test_unhandled_errors(sentry_init, capture_exceptions, capture_events):
sentry_init(integrations=[FalconIntegration()])
class Resource:
def on_get(self, req, resp):
1 / 0
app = falcon.API()
app.add_route("/", Resource())
exceptions = capture_exceptions()
events = capture_events()
client = falcon.testing.TestClient(app)
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"]
def test_raised_5xx_errors(sentry_init, capture_exceptions, capture_events):
sentry_init(integrations=[FalconIntegration()])
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_502)
app = falcon.API()
app.add_route("/", Resource())
exceptions = capture_exceptions()
events = capture_events()
client = falcon.testing.TestClient(app)
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"
def test_raised_4xx_errors(sentry_init, capture_exceptions, capture_events):
sentry_init(integrations=[FalconIntegration()])
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_400)
app = falcon.API()
app.add_route("/", Resource())
exceptions = capture_exceptions()
events = capture_events()
client = falcon.testing.TestClient(app)
client.simulate_get("/")
assert len(exceptions) == 0
assert len(events) == 0
def test_http_status(sentry_init, capture_exceptions, capture_events):
"""
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()])
class Resource:
def on_get(self, req, resp):
raise falcon.http_status.HTTPStatus(falcon.HTTP_508)
app = falcon.API()
app.add_route("/", Resource())
exceptions = capture_exceptions()
events = capture_events()
client = falcon.testing.TestClient(app)
client.simulate_get("/")
assert len(exceptions) == 0
assert len(events) == 0
def test_falcon_large_json_request(sentry_init, capture_events):
sentry_init(integrations=[FalconIntegration()])
data = {"foo": {"bar": "a" * 2000}}
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())
events = capture_events()
client = falcon.testing.TestClient(app)
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = events
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
def test_falcon_empty_json_request(sentry_init, capture_events, data):
sentry_init(integrations=[FalconIntegration()])
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())
events = capture_events()
client = falcon.testing.TestClient(app)
response = client.simulate_post("/", json=data)
assert response.status == falcon.HTTP_200
(event,) = events
assert event["request"]["data"] == data
def test_falcon_raw_data_request(sentry_init, capture_events):
sentry_init(integrations=[FalconIntegration()])
class Resource:
def on_post(self, req, resp):
sentry_sdk.capture_message("hi")
resp.media = "ok"
app = falcon.API()
app.add_route("/", Resource())
events = capture_events()
client = falcon.testing.TestClient(app)
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"] == ""
def test_logging(sentry_init, capture_events):
sentry_init(
integrations=[FalconIntegration(), LoggingIntegration(event_level="ERROR")]
)
logger = logging.getLogger()
app = falcon.API()
class Resource:
def on_get(self, req, resp):
logger.error("hi")
resp.media = "ok"
app.add_route("/", Resource())
events = capture_events()
client = falcon.testing.TestClient(app)
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."}
def test_error_in_errorhandler(sentry_init, capture_events):
sentry_init(integrations=[FalconIntegration()])
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)
events = capture_events()
client = falcon.testing.TestClient(app)
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()"
def test_bad_request_not_captured(sentry_init, capture_events):
sentry_init(integrations=[FalconIntegration()])
events = capture_events()
app = falcon.API()
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPBadRequest()
app.add_route("/", Resource())
client = falcon.testing.TestClient(app)
client.simulate_get("/")
assert not events
def test_does_not_leak_scope(sentry_init, capture_events):
sentry_init(integrations=[FalconIntegration()])
events = capture_events()
sentry_sdk.get_isolation_scope().set_tag("request_data", False)
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)
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."
)
def test_falcon_not_breaking_asgi(sentry_init):
"""
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()])
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+",
)
def test_falcon_custom_error_handler(sentry_init, make_app, capture_events):
"""
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()])
events = capture_events()
app = make_app()
client = falcon.testing.TestClient(app)
client.simulate_get("/custom-error")
assert len(events) == 0
def test_span_origin(sentry_init, capture_events, make_client):
sentry_init(
integrations=[FalconIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = make_client()
client.simulate_get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.falcon"
sentry-python-2.18.0/tests/integrations/fastapi/ 0000775 0000000 0000000 00000000000 14712146540 0021750 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/fastapi/__init__.py 0000664 0000000 0000000 00000000056 14712146540 0024062 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("fastapi")
sentry-python-2.18.0/tests/integrations/fastapi/test_fastapi.py 0000664 0000000 0000000 00000045064 14712146540 0025021 0 ustar 00root root 0000000 0000000 import json
import logging
import pytest
import threading
import warnings
from unittest import mock
import fastapi
from fastapi import FastAPI, HTTPException, Request
from fastapi.testclient import TestClient
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from sentry_sdk import capture_message
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from sentry_sdk.utils import parse_version
FASTAPI_VERSION = parse_version(fastapi.__version__)
from tests.integrations.starlette import test_starlette
def fastapi_app_factory():
app = FastAPI()
@app.get("/error")
async def _error():
capture_message("Hi")
1 / 0
return {"message": "Hi"}
@app.get("/message")
async def _message():
capture_message("Hi")
return {"message": "Hi"}
@app.delete("/nomessage")
@app.get("/nomessage")
@app.head("/nomessage")
@app.options("/nomessage")
@app.patch("/nomessage")
@app.post("/nomessage")
@app.put("/nomessage")
@app.trace("/nomessage")
async def _nomessage():
return {"message": "nothing here..."}
@app.get("/message/{message_id}")
async def _message_with_id(message_id):
capture_message("Hi")
return {"message": "Hi"}
@app.get("/sync/thread_ids")
def _thread_ids_sync():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}
@app.get("/async/thread_ids")
async def _thread_ids_async():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}
return app
@pytest.mark.asyncio
async def test_response(sentry_init, capture_events):
# FastAPI is heavily based on Starlette so we also need
# to enable StarletteIntegration.
# In the future this will be auto enabled.
sentry_init(
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
app = fastapi_app_factory()
events = capture_events()
client = TestClient(app)
response = client.get("/message")
assert response.json() == {"message": "Hi"}
assert len(events) == 2
(message_event, transaction_event) = events
assert message_event["message"] == "Hi"
assert transaction_event["transaction"] == "/message"
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
(
"/message",
"url",
"/message",
"route",
),
(
"/message",
"endpoint",
"tests.integrations.fastapi.test_fastapi.fastapi_app_factory.._message",
"component",
),
(
"/message/123456",
"url",
"/message/{message_id}",
"route",
),
(
"/message/123456",
"endpoint",
"tests.integrations.fastapi.test_fastapi.fastapi_app_factory.._message_with_id",
"component",
),
],
)
def test_transaction_style(
sentry_init,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(
integrations=[
StarletteIntegration(transaction_style=transaction_style),
FastApiIntegration(transaction_style=transaction_style),
],
)
app = fastapi_app_factory()
events = capture_events()
client = TestClient(app)
client.get(url)
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
# Assert that state is not leaked
events.clear()
capture_message("foo")
(event,) = events
assert "request" not in event
assert "transaction" not in event
def test_legacy_setup(
sentry_init,
capture_events,
):
# Check that behaviour does not change
# if the user just adds the new Integrations
# and forgets to remove SentryAsgiMiddleware
sentry_init()
app = fastapi_app_factory()
asgi_app = SentryAsgiMiddleware(app)
events = capture_events()
client = TestClient(asgi_app)
client.get("/message/123456")
(event,) = events
assert event["transaction"] == "/message/{message_id}"
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0)
def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint):
sentry_init(
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
app = fastapi_app_factory()
asgi_app = SentryAsgiMiddleware(app)
envelopes = capture_envelopes()
client = TestClient(asgi_app)
response = client.get(endpoint)
assert response.status_code == 200
data = json.loads(response.content)
envelopes = [envelope for envelope in envelopes]
assert len(envelopes) == 1
profiles = [item for item in envelopes[0].items if item.type == "profile"]
assert len(profiles) == 1
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
async def test_original_request_not_scrubbed(sentry_init, capture_events):
sentry_init(
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=1.0,
)
app = FastAPI()
@app.post("/error")
async def _error(request: Request):
logging.critical("Oh no!")
assert request.headers["Authorization"] == "Bearer ohno"
assert await request.json() == {"password": "secret"}
return {"error": "Oh no!"}
events = capture_events()
client = TestClient(app)
client.post(
"/error", json={"password": "secret"}, headers={"Authorization": "Bearer ohno"}
)
event = events[0]
assert event["request"]["data"] == {"password": "[Filtered]"}
assert event["request"]["headers"]["authorization"] == "[Filtered]"
@pytest.mark.asyncio
def test_response_status_code_ok_in_transaction_context(sentry_init, capture_envelopes):
"""
Tests that the response status code is added to the transaction "response" context.
"""
sentry_init(
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=1.0,
release="demo-release",
)
envelopes = capture_envelopes()
app = fastapi_app_factory()
client = TestClient(app)
client.get("/message")
(_, transaction_envelope) = envelopes
transaction = transaction_envelope.get_transaction_event()
assert transaction["type"] == "transaction"
assert len(transaction["contexts"]) > 0
assert (
"response" in transaction["contexts"].keys()
), "Response context not found in transaction"
assert transaction["contexts"]["response"]["status_code"] == 200
@pytest.mark.asyncio
def test_response_status_code_error_in_transaction_context(
sentry_init,
capture_envelopes,
):
"""
Tests that the response status code is added to the transaction "response" context.
"""
sentry_init(
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=1.0,
release="demo-release",
)
envelopes = capture_envelopes()
app = fastapi_app_factory()
client = TestClient(app)
with pytest.raises(ZeroDivisionError):
client.get("/error")
(
_,
_,
transaction_envelope,
) = envelopes
transaction = transaction_envelope.get_transaction_event()
assert transaction["type"] == "transaction"
assert len(transaction["contexts"]) > 0
assert (
"response" in transaction["contexts"].keys()
), "Response context not found in transaction"
assert transaction["contexts"]["response"]["status_code"] == 500
@pytest.mark.asyncio
def test_response_status_code_not_found_in_transaction_context(
sentry_init,
capture_envelopes,
):
"""
Tests that the response status code is added to the transaction "response" context.
"""
sentry_init(
integrations=[StarletteIntegration(), FastApiIntegration()],
traces_sample_rate=1.0,
release="demo-release",
)
envelopes = capture_envelopes()
app = fastapi_app_factory()
client = TestClient(app)
client.get("/non-existing-route-123")
(transaction_envelope,) = envelopes
transaction = transaction_envelope.get_transaction_event()
assert transaction["type"] == "transaction"
assert len(transaction["contexts"]) > 0
assert (
"response" in transaction["contexts"].keys()
), "Response context not found in transaction"
assert transaction["contexts"]["response"]["status_code"] == 404
@pytest.mark.parametrize(
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
[
(
"/message/123456",
"endpoint",
"tests.integrations.fastapi.test_fastapi.fastapi_app_factory.._message_with_id",
"component",
),
(
"/message/123456",
"url",
"/message/{message_id}",
"route",
),
],
)
def test_transaction_name(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
capture_envelopes,
):
"""
Tests that the transaction name is something meaningful.
"""
sentry_init(
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
integrations=[
StarletteIntegration(transaction_style=transaction_style),
FastApiIntegration(transaction_style=transaction_style),
],
traces_sample_rate=1.0,
)
envelopes = capture_envelopes()
app = fastapi_app_factory()
client = TestClient(app)
client.get(request_url)
(_, 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
)
def test_route_endpoint_equal_dependant_call(sentry_init):
"""
Tests that the route endpoint name is equal to the wrapped dependant call name.
"""
sentry_init(
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
integrations=[
StarletteIntegration(),
FastApiIntegration(),
],
traces_sample_rate=1.0,
)
app = fastapi_app_factory()
for route in app.router.routes:
if not hasattr(route, "dependant"):
continue
assert route.endpoint.__qualname__ == route.dependant.call.__qualname__
@pytest.mark.parametrize(
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
[
(
"/message/123456",
"endpoint",
"http://testserver/message/123456",
"url",
),
(
"/message/123456",
"url",
"http://testserver/message/123456",
"url",
),
],
)
def test_transaction_name_in_traces_sampler(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
):
"""
Tests that a custom traces_sampler retrieves 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):
assert (
sampling_context["transaction_context"]["name"] == expected_transaction_name
)
assert (
sampling_context["transaction_context"]["source"]
== expected_transaction_source
)
sentry_init(
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
integrations=[StarletteIntegration(transaction_style=transaction_style)],
traces_sampler=dummy_traces_sampler,
traces_sample_rate=1.0,
)
app = fastapi_app_factory()
client = TestClient(app)
client.get(request_url)
@pytest.mark.parametrize(
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
[
(
"/message/123456",
"endpoint",
"starlette.middleware.trustedhost.TrustedHostMiddleware",
"component",
),
(
"/message/123456",
"url",
"http://testserver/message/123456",
"url",
),
],
)
def test_transaction_name_in_middleware(
sentry_init,
request_url,
transaction_style,
expected_transaction_name,
expected_transaction_source,
capture_envelopes,
):
"""
Tests that the transaction name is something meaningful.
"""
sentry_init(
auto_enabling_integrations=False, # Make sure that httpx integration is not added, because it adds tracing information to the starlette test clients request.
integrations=[
StarletteIntegration(transaction_style=transaction_style),
FastApiIntegration(transaction_style=transaction_style),
],
traces_sample_rate=1.0,
)
envelopes = capture_envelopes()
app = fastapi_app_factory()
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=[
"example.com",
],
)
client = TestClient(app)
client.get(request_url)
(transaction_envelope,) = envelopes
transaction_event = transaction_envelope.get_transaction_event()
assert transaction_event["contexts"]["response"]["status_code"] == 400
assert transaction_event["transaction"] == expected_transaction_name
assert (
transaction_event["transaction_info"]["source"] == expected_transaction_source
)
@test_starlette.parametrize_test_configurable_status_codes_deprecated
def test_configurable_status_codes_deprecated(
sentry_init,
capture_events,
failed_request_status_codes,
status_code,
expected_error,
):
with pytest.warns(DeprecationWarning):
starlette_integration = StarletteIntegration(
failed_request_status_codes=failed_request_status_codes
)
with pytest.warns(DeprecationWarning):
fast_api_integration = FastApiIntegration(
failed_request_status_codes=failed_request_status_codes
)
sentry_init(
integrations=[
starlette_integration,
fast_api_integration,
]
)
events = capture_events()
app = FastAPI()
@app.get("/error")
async def _error():
raise HTTPException(status_code)
client = TestClient(app)
client.get("/error")
if expected_error:
assert len(events) == 1
else:
assert not events
@pytest.mark.skipif(
FASTAPI_VERSION < (0, 80),
reason="Requires FastAPI >= 0.80, because earlier versions do not support HTTP 'HEAD' requests",
)
def test_transaction_http_method_default(sentry_init, capture_events):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
# FastAPI is heavily based on Starlette so we also need
# to enable StarletteIntegration.
# In the future this will be auto enabled.
sentry_init(
traces_sample_rate=1.0,
integrations=[
StarletteIntegration(),
FastApiIntegration(),
],
)
app = fastapi_app_factory()
events = capture_events()
client = TestClient(app)
client.get("/nomessage")
client.options("/nomessage")
client.head("/nomessage")
assert len(events) == 1
(event,) = events
assert event["request"]["method"] == "GET"
@pytest.mark.skipif(
FASTAPI_VERSION < (0, 80),
reason="Requires FastAPI >= 0.80, because earlier versions do not support HTTP 'HEAD' requests",
)
def test_transaction_http_method_custom(sentry_init, capture_events):
# FastAPI is heavily based on Starlette so we also need
# to enable StarletteIntegration.
# In the future this will be auto enabled.
sentry_init(
traces_sample_rate=1.0,
integrations=[
StarletteIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
),
FastApiIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
), # capitalization does not matter
),
],
)
app = fastapi_app_factory()
events = capture_events()
client = TestClient(app)
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"
@test_starlette.parametrize_test_configurable_status_codes
def test_configurable_status_codes(
sentry_init,
capture_events,
failed_request_status_codes,
status_code,
expected_error,
):
integration_kwargs = {}
if failed_request_status_codes is not None:
integration_kwargs["failed_request_status_codes"] = failed_request_status_codes
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
starlette_integration = StarletteIntegration(**integration_kwargs)
fastapi_integration = FastApiIntegration(**integration_kwargs)
sentry_init(integrations=[starlette_integration, fastapi_integration])
events = capture_events()
app = FastAPI()
@app.get("/error")
async def _error():
raise HTTPException(status_code)
client = TestClient(app)
client.get("/error")
assert len(events) == int(expected_error)
sentry-python-2.18.0/tests/integrations/flask/ 0000775 0000000 0000000 00000000000 14712146540 0021421 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/flask/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0023531 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("flask")
sentry-python-2.18.0/tests/integrations/flask/test_flask.py 0000664 0000000 0000000 00000066512 14712146540 0024144 0 ustar 00root root 0000000 0000000 import json
import re
import logging
from io import BytesIO
import pytest
from flask import (
Flask,
Response,
request,
abort,
stream_with_context,
render_template_string,
)
from flask.views import View
from flask_login import LoginManager, login_user
try:
from werkzeug.wrappers.request import UnsupportedMediaType
except ImportError:
UnsupportedMediaType = None
import sentry_sdk
import sentry_sdk.integrations.flask as flask_sentry
from sentry_sdk import (
set_tag,
capture_message,
capture_exception,
)
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
login_manager = LoginManager()
@pytest.fixture
def app():
app = Flask(__name__)
app.config["TESTING"] = True
app.secret_key = "haha"
login_manager.init_app(app)
@app.route("/message")
def hi():
capture_message("hi")
return "ok"
@app.route("/nomessage")
def nohi():
return "ok"
@app.route("/message/")
def hi_with_id(message_id):
capture_message("hi again")
return "ok"
return app
@pytest.fixture(params=("auto", "manual"))
def integration_enabled_params(request):
if request.param == "auto":
return {"auto_enabling_integrations": True}
elif request.param == "manual":
return {"integrations": [flask_sentry.FlaskIntegration()]}
else:
raise ValueError(request.param)
def test_has_context(sentry_init, app, capture_events):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
client = app.test_client()
response = client.get("/message")
assert response.status_code == 200
(event,) = events
assert event["transaction"] == "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", "endpoint", "hi_with_id", "component"),
("/message/123456", "url", "/message/", "route"),
],
)
def test_transaction_style(
sentry_init,
app,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(
integrations=[
flask_sentry.FlaskIntegration(transaction_style=transaction_style)
]
)
events = capture_events()
client = app.test_client()
response = client.get(url)
assert response.status_code == 200
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
@pytest.mark.parametrize("debug", (True, False))
@pytest.mark.parametrize("testing", (True, False))
def test_errors(
sentry_init,
capture_exceptions,
capture_events,
app,
debug,
testing,
integration_enabled_params,
):
sentry_init(**integration_enabled_params)
app.debug = debug
app.testing = testing
@app.route("/")
def index():
1 / 0
exceptions = capture_exceptions()
events = capture_events()
client = app.test_client()
try:
client.get("/")
except ZeroDivisionError:
pass
(exc,) = exceptions
assert isinstance(exc, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "flask"
def test_flask_login_not_installed(
sentry_init, app, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
monkeypatch.setattr(flask_sentry, "flask_login", None)
events = capture_events()
client = app.test_client()
client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
def test_flask_login_not_configured(
sentry_init, app, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
assert flask_sentry.flask_login
events = capture_events()
client = app.test_client()
client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
def test_flask_login_partially_configured(
sentry_init, app, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
events = capture_events()
login_manager = LoginManager()
login_manager.init_app(app)
client = app.test_client()
client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
@pytest.mark.parametrize("send_default_pii", [True, False])
@pytest.mark.parametrize("user_id", [None, "42", 3])
def test_flask_login_configured(
send_default_pii,
sentry_init,
app,
user_id,
capture_events,
monkeypatch,
integration_enabled_params,
):
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
class User:
is_authenticated = is_active = True
is_anonymous = user_id is not None
def get_id(self):
return str(user_id)
@login_manager.user_loader
def load_user(user_id):
if user_id is not None:
return User()
@app.route("/login")
def login():
if user_id is not None:
login_user(User())
return "ok"
events = capture_events()
client = app.test_client()
assert client.get("/login").status_code == 200
assert not events
assert client.get("/message").status_code == 200
(event,) = events
if user_id is None or not send_default_pii:
assert event.get("user", {}).get("id") is None
else:
assert event["user"]["id"] == str(user_id)
def test_flask_large_json_request(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
data = {"foo": {"bar": "a" * 2000}}
@app.route("/", methods=["POST"])
def index():
assert request.get_json() == data
assert request.get_data() == json.dumps(data).encode("ascii")
assert not request.form
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response.status_code == 200
(event,) = events
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
def test_flask_session_tracking(sentry_init, capture_envelopes, app):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
release="demo-release",
)
@app.route("/")
def index():
sentry_sdk.get_isolation_scope().set_user({"ip_address": "1.2.3.4", "id": "42"})
try:
raise ValueError("stuff")
except Exception:
logging.exception("stuff happened")
1 / 0
envelopes = capture_envelopes()
with app.test_client() as client:
try:
client.get("/", headers={"User-Agent": "blafasel/1.0"})
except ZeroDivisionError:
pass
sentry_sdk.get_client().flush()
(first_event, error_event, session) = envelopes
first_event = first_event.get_event()
error_event = error_event.get_event()
session = session.items[0].payload.json
aggregates = session["aggregates"]
assert first_event["exception"]["values"][0]["type"] == "ValueError"
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert len(aggregates) == 1
assert aggregates[0]["crashed"] == 1
assert aggregates[0]["started"]
assert session["attrs"]["release"] == "demo-release"
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
def test_flask_empty_json_request(sentry_init, capture_events, app, data):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
@app.route("/", methods=["POST"])
def index():
assert request.get_json() == data
assert request.get_data() == json.dumps(data).encode("ascii")
assert not request.form
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response.status_code == 200
(event,) = events
assert event["request"]["data"] == data
def test_flask_medium_formdata_request(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
data = {"foo": "a" * 2000}
@app.route("/", methods=["POST"])
def index():
assert request.form["foo"] == data["foo"]
assert not request.get_data()
try:
assert not request.get_json()
except UnsupportedMediaType:
# flask/werkzeug 3
pass
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200
(event,) = events
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]) == 1024
def test_flask_formdata_request_appear_transaction_body(
sentry_init, capture_events, app
):
"""
Test that ensures that transaction request data contains body, even if no exception was raised
"""
sentry_init(integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0)
data = {"username": "sentry-user", "age": "26"}
@app.route("/", methods=["POST"])
def index():
assert request.form["username"] == data["username"]
assert request.form["age"] == data["age"]
assert not request.get_data()
try:
assert not request.get_json()
except UnsupportedMediaType:
# flask/werkzeug 3
pass
set_tag("view", "yes")
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200
event, transaction_event = events
assert "request" in transaction_event
assert "data" in transaction_event["request"]
assert transaction_event["request"]["data"] == data
@pytest.mark.parametrize("input_char", ["a", b"a"])
def test_flask_too_large_raw_request(sentry_init, input_char, capture_events, app):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="small"
)
data = input_char * 2000
@app.route("/", methods=["POST"])
def index():
assert not request.form
if isinstance(data, bytes):
assert request.get_data() == data
else:
assert request.get_data() == data.encode("ascii")
try:
assert not request.get_json()
except UnsupportedMediaType:
# flask/werkzeug 3
pass
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200
(event,) = events
assert event["_meta"]["request"]["data"] == {"": {"rem": [["!config", "x"]]}}
assert not event["request"]["data"]
def test_flask_files_and_form(sentry_init, capture_events, app):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always"
)
data = {"foo": "a" * 2000, "file": (BytesIO(b"hello"), "hello.txt")}
@app.route("/", methods=["POST"])
def index():
assert list(request.form) == ["foo"]
assert list(request.files) == ["file"]
try:
assert not request.get_json()
except UnsupportedMediaType:
# flask/werkzeug 3
pass
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", data=data)
assert response.status_code == 200
(event,) = events
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]) == 1024
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
):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always"
)
data = {
"key{}".format(i): "value{}".format(i) for i in range(MAX_DATABAG_BREADTH + 10)
}
@app.route("/", methods=["POST"])
def index():
assert request.get_json() == data
assert request.get_data() == json.dumps(data).encode("ascii")
capture_message("hi")
return "ok"
events = capture_events()
client = app.test_client()
response = client.post("/", content_type="application/json", data=json.dumps(data))
assert response.status_code == 200
(event,) = events
assert event["request"]["data"] == data
@pytest.mark.parametrize(
"integrations",
[
[flask_sentry.FlaskIntegration()],
[flask_sentry.FlaskIntegration(), LoggingIntegration(event_level="ERROR")],
],
)
def test_errors_not_reported_twice(sentry_init, integrations, capture_events, app):
sentry_init(integrations=integrations)
@app.route("/")
def index():
try:
1 / 0
except Exception as e:
app.logger.exception(e)
raise e
events = capture_events()
client = app.test_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
assert len(events) == 1
def test_logging(sentry_init, capture_events, app):
# ensure that Flask's logger magic doesn't break ours
sentry_init(
integrations=[
flask_sentry.FlaskIntegration(),
LoggingIntegration(event_level="ERROR"),
]
)
@app.route("/")
def index():
app.logger.error("hi")
return "ok"
events = capture_events()
client = app.test_client()
client.get("/")
(event,) = events
assert event["level"] == "error"
def test_no_errors_without_request(app, sentry_init):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
with app.app_context():
capture_exception(ValueError())
def test_cli_commands_raise(app):
if not hasattr(app, "cli"):
pytest.skip("Too old flask version")
from flask.cli import ScriptInfo
@app.cli.command()
def foo():
1 / 0
def create_app(*_):
return app
with pytest.raises(ZeroDivisionError):
app.cli.main(
args=["foo"], prog_name="myapp", obj=ScriptInfo(create_app=create_app)
)
def test_wsgi_level_error_is_caught(
app, capture_exceptions, capture_events, sentry_init
):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
def wsgi_app(environ, start_response):
1 / 0
app.wsgi_app = wsgi_app
client = app.test_client()
exceptions = capture_exceptions()
events = capture_events()
with pytest.raises(ZeroDivisionError) as exc:
client.get("/")
(error,) = exceptions
assert error is exc.value
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "wsgi"
def test_500(sentry_init, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
app.debug = False
app.testing = False
@app.route("/")
def index():
1 / 0
@app.errorhandler(500)
def error_handler(err):
return "Sentry error."
client = app.test_client()
response = client.get("/")
assert response.data.decode("utf-8") == "Sentry error."
def test_error_in_errorhandler(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
app.debug = False
app.testing = False
@app.route("/")
def index():
raise ValueError()
@app.errorhandler(500)
def error_handler(err):
1 / 0
events = capture_events()
client = app.test_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
event1, event2 = events
(exception,) = event1["exception"]["values"]
assert exception["type"] == "ValueError"
exception = event2["exception"]["values"][-1]
assert exception["type"] == "ZeroDivisionError"
def test_bad_request_not_captured(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/")
def index():
abort(400)
client = app.test_client()
client.get("/")
assert not events
def test_does_not_leak_scope(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
sentry_sdk.get_isolation_scope().set_tag("request_data", False)
@app.route("/")
def index():
sentry_sdk.get_isolation_scope().set_tag("request_data", True)
def generate():
for row in range(1000):
assert sentry_sdk.get_isolation_scope()._tags["request_data"]
yield str(row) + "\n"
return Response(stream_with_context(generate()), mimetype="text/csv")
client = app.test_client()
response = client.get("/")
assert response.data.decode() == "".join(str(row) + "\n" for row in range(1000))
assert not events
assert not sentry_sdk.get_isolation_scope()._tags["request_data"]
def test_scoped_test_client(sentry_init, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
@app.route("/")
def index():
return "ok"
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
@pytest.mark.parametrize("exc_cls", [ZeroDivisionError, Exception])
def test_errorhandler_for_exception_swallows_exception(
sentry_init, app, capture_events, exc_cls
):
# In contrast to error handlers for a status code, error
# handlers for exceptions can swallow the exception (this is
# just how the Flask signal works)
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/")
def index():
1 / 0
@app.errorhandler(exc_cls)
def zerodivision(e):
return "ok"
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
assert not events
def test_tracing_success(sentry_init, capture_events, app):
sentry_init(traces_sample_rate=1.0, integrations=[flask_sentry.FlaskIntegration()])
@app.before_request
def _():
set_tag("before_request", "yes")
@app.route("/message_tx")
def hi_tx():
set_tag("view", "yes")
capture_message("hi")
return "ok"
events = capture_events()
with app.test_client() as client:
response = client.get("/message_tx")
assert response.status_code == 200
message_event, transaction_event = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "hi_tx"
assert transaction_event["contexts"]["trace"]["status"] == "ok"
assert transaction_event["tags"]["view"] == "yes"
assert transaction_event["tags"]["before_request"] == "yes"
assert message_event["message"] == "hi"
assert message_event["transaction"] == "hi_tx"
assert message_event["tags"]["view"] == "yes"
assert message_event["tags"]["before_request"] == "yes"
def test_tracing_error(sentry_init, capture_events, app):
sentry_init(traces_sample_rate=1.0, integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/error")
def error():
1 / 0
with pytest.raises(ZeroDivisionError):
with app.test_client() as client:
response = client.get("/error")
assert response.status_code == 500
error_event, transaction_event = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "error"
assert transaction_event["contexts"]["trace"]["status"] == "internal_error"
assert error_event["transaction"] == "error"
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
def test_error_has_trace_context_if_tracing_disabled(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/error")
def error():
1 / 0
with pytest.raises(ZeroDivisionError):
with app.test_client() as client:
response = client.get("/error")
assert response.status_code == 500
(error_event,) = events
assert error_event["contexts"]["trace"]
def test_class_based_views(sentry_init, app, capture_events):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/")
class HelloClass(View):
def dispatch_request(self):
capture_message("hi")
return "ok"
app.add_url_rule("/hello-class/", view_func=HelloClass.as_view("hello_class"))
with app.test_client() as client:
response = client.get("/hello-class/")
assert response.status_code == 200
(event,) = events
assert event["message"] == "hi"
assert event["transaction"] == "hello_class"
@pytest.mark.parametrize(
"template_string", ["{{ sentry_trace }}", "{{ sentry_trace_meta }}"]
)
def test_template_tracing_meta(sentry_init, app, capture_events, template_string):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
events = capture_events()
@app.route("/")
def index():
capture_message(sentry_sdk.get_traceparent() + "\n" + sentry_sdk.get_baggage())
return render_template_string(template_string)
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
rendered_meta = response.data.decode("utf-8")
traceparent, baggage = events[0]["message"].split("\n")
assert traceparent != ""
assert baggage != ""
match = re.match(
r'^',
rendered_meta,
)
assert match is not None
assert match.group(1) == traceparent
rendered_baggage = match.group(2)
assert rendered_baggage == baggage
def test_dont_override_sentry_trace_context(sentry_init, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
@app.route("/")
def index():
return render_template_string("{{ sentry_trace }}", sentry_trace="hi")
with app.test_client() as client:
response = client.get("/")
assert response.status_code == 200
assert response.data == b"hi"
def test_request_not_modified_by_reference(sentry_init, capture_events, app):
sentry_init(integrations=[flask_sentry.FlaskIntegration()])
@app.route("/", methods=["POST"])
def index():
logging.critical("oops")
assert request.get_json() == {"password": "ohno"}
assert request.headers["Authorization"] == "Bearer ohno"
return "ok"
events = capture_events()
client = app.test_client()
client.post(
"/", json={"password": "ohno"}, headers={"Authorization": "Bearer ohno"}
)
(event,) = events
assert event["request"]["data"]["password"] == "[Filtered]"
assert event["request"]["headers"]["Authorization"] == "[Filtered]"
def test_response_status_code_ok_in_transaction_context(
sentry_init, capture_envelopes, app
):
"""
Tests that the response status code is added to the transaction context.
This also works for when there is an Exception during the request, but somehow the test flask app doesn't seem to trigger that.
"""
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
traces_sample_rate=1.0,
release="demo-release",
)
envelopes = capture_envelopes()
client = app.test_client()
client.get("/message")
sentry_sdk.get_client().flush()
(_, transaction_envelope, _) = envelopes
transaction = transaction_envelope.get_transaction_event()
assert transaction["type"] == "transaction"
assert len(transaction["contexts"]) > 0
assert (
"response" in transaction["contexts"].keys()
), "Response context not found in transaction"
assert transaction["contexts"]["response"]["status_code"] == 200
def test_response_status_code_not_found_in_transaction_context(
sentry_init, capture_envelopes, app
):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
traces_sample_rate=1.0,
release="demo-release",
)
envelopes = capture_envelopes()
client = app.test_client()
client.get("/not-existing-route")
sentry_sdk.get_client().flush()
(transaction_envelope, _) = envelopes
transaction = transaction_envelope.get_transaction_event()
assert transaction["type"] == "transaction"
assert len(transaction["contexts"]) > 0
assert (
"response" in transaction["contexts"].keys()
), "Response context not found in transaction"
assert transaction["contexts"]["response"]["status_code"] == 404
def test_span_origin(sentry_init, app, capture_events):
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = app.test_client()
client.get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.flask"
def test_transaction_http_method_default(
sentry_init,
app,
capture_events,
):
"""
By default OPTIONS and HEAD requests do not create a transaction.
"""
sentry_init(
traces_sample_rate=1.0,
integrations=[flask_sentry.FlaskIntegration()],
)
events = capture_events()
client = app.test_client()
response = client.get("/nomessage")
assert response.status_code == 200
response = client.options("/nomessage")
assert response.status_code == 200
response = client.head("/nomessage")
assert response.status_code == 200
(event,) = events
assert len(events) == 1
assert event["request"]["method"] == "GET"
def test_transaction_http_method_custom(
sentry_init,
app,
capture_events,
):
"""
Configure FlaskIntegration to ONLY capture OPTIONS and HEAD requests.
"""
sentry_init(
traces_sample_rate=1.0,
integrations=[
flask_sentry.FlaskIntegration(
http_methods_to_capture=(
"OPTIONS",
"head",
) # capitalization does not matter
) # case does not matter
],
)
events = capture_events()
client = app.test_client()
response = client.get("/nomessage")
assert response.status_code == 200
response = client.options("/nomessage")
assert response.status_code == 200
response = client.head("/nomessage")
assert response.status_code == 200
assert len(events) == 2
(event1, event2) = events
assert event1["request"]["method"] == "OPTIONS"
assert event2["request"]["method"] == "HEAD"
sentry-python-2.18.0/tests/integrations/gcp/ 0000775 0000000 0000000 00000000000 14712146540 0021072 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/gcp/test_gcp.py 0000664 0000000 0000000 00000042306 14712146540 0023261 0 ustar 00root root 0000000 0000000 """
# GCP Cloud Functions unit tests
"""
import json
from textwrap import dedent
import tempfile
import sys
import subprocess
import pytest
import os.path
import os
FUNCTIONS_PRELUDE = """
from unittest.mock import Mock
import __main__ as gcp_functions
import os
# Initializing all the necessary environment variables
os.environ["FUNCTION_TIMEOUT_SEC"] = "3"
os.environ["FUNCTION_NAME"] = "Google Cloud function"
os.environ["ENTRY_POINT"] = "cloud_function"
os.environ["FUNCTION_IDENTITY"] = "func_ID"
os.environ["FUNCTION_REGION"] = "us-central1"
os.environ["GCP_PROJECT"] = "serverless_project"
def log_return_value(func):
def inner(*args, **kwargs):
rv = func(*args, **kwargs)
print("\\nRETURN VALUE: {}\\n".format(json.dumps(rv)))
return rv
return inner
gcp_functions.worker_v1 = Mock()
gcp_functions.worker_v1.FunctionHandler = Mock()
gcp_functions.worker_v1.FunctionHandler.invoke_user_function = log_return_value(cloud_function)
import sentry_sdk
from sentry_sdk.integrations.gcp import GcpIntegration
import json
import time
from sentry_sdk.transport import HttpTransport
def event_processor(event):
# Adding delay which would allow us to capture events.
time.sleep(1)
return event
def envelope_processor(envelope):
(item,) = envelope.items
return item.get_bytes()
class TestTransport(HttpTransport):
def capture_envelope(self, envelope):
envelope_item = envelope_processor(envelope)
print("\\nENVELOPE: {}\\n".format(envelope_item.decode(\"utf-8\")))
def init_sdk(timeout_warning=False, **extra_init_args):
sentry_sdk.init(
dsn="https://123abc@example.com/123",
transport=TestTransport,
integrations=[GcpIntegration(timeout_warning=timeout_warning)],
shutdown_timeout=10,
# excepthook -> dedupe -> event_processor client report gets added
# which we don't really care about for these tests
send_client_reports=False,
**extra_init_args
)
"""
@pytest.fixture
def run_cloud_function():
def inner(code, subprocess_kwargs=()):
envelope_items = []
return_value = None
# STEP : Create a zip of cloud function
subprocess_kwargs = dict(subprocess_kwargs)
with tempfile.TemporaryDirectory() as tmpdir:
main_py = os.path.join(tmpdir, "main.py")
with open(main_py, "w") as f:
f.write(code)
setup_cfg = os.path.join(tmpdir, "setup.cfg")
with open(setup_cfg, "w") as f:
f.write("[install]\nprefix=")
subprocess.check_call(
[sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")],
**subprocess_kwargs
)
subprocess.check_call(
"pip install ../*.tar.gz -t .",
cwd=tmpdir,
shell=True,
**subprocess_kwargs
)
stream = os.popen("python {}/main.py".format(tmpdir))
stream_data = stream.read()
stream.close()
for line in stream_data.splitlines():
print("GCP:", line)
if line.startswith("ENVELOPE: "):
line = line[len("ENVELOPE: ") :]
envelope_items.append(json.loads(line))
elif line.startswith("RETURN VALUE: "):
line = line[len("RETURN VALUE: ") :]
return_value = json.loads(line)
else:
continue
stream.close()
return envelope_items, return_value
return inner
def test_handled_exception(run_cloud_function):
envelope_items, return_value = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
raise Exception("something went wrong")
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(timeout_warning=False)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert envelope_items[0]["level"] == "error"
(exception,) = envelope_items[0]["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "something went wrong"
assert exception["mechanism"]["type"] == "gcp"
assert not exception["mechanism"]["handled"]
def test_unhandled_exception(run_cloud_function):
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
x = 3/0
return "3"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(timeout_warning=False)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert envelope_items[0]["level"] == "error"
(exception,) = envelope_items[0]["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["value"] == "division by zero"
assert exception["mechanism"]["type"] == "gcp"
assert not exception["mechanism"]["handled"]
def test_timeout_error(run_cloud_function):
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
time.sleep(10)
return "3"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(timeout_warning=True)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert envelope_items[0]["level"] == "error"
(exception,) = envelope_items[0]["exception"]["values"]
assert exception["type"] == "ServerlessTimeoutWarning"
assert (
exception["value"]
== "WARNING : Function is expected to get timed out. Configured timeout duration = 3 seconds."
)
assert exception["mechanism"]["type"] == "threading"
assert not exception["mechanism"]["handled"]
def test_performance_no_error(run_cloud_function):
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
return "test_string"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert envelope_items[0]["type"] == "transaction"
assert envelope_items[0]["contexts"]["trace"]["op"] == "function.gcp"
assert envelope_items[0]["transaction"].startswith("Google Cloud function")
assert envelope_items[0]["transaction_info"] == {"source": "component"}
assert envelope_items[0]["transaction"] in envelope_items[0]["request"]["url"]
def test_performance_error(run_cloud_function):
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
raise Exception("something went wrong")
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert envelope_items[0]["level"] == "error"
(exception,) = envelope_items[0]["exception"]["values"]
assert exception["type"] == "Exception"
assert exception["value"] == "something went wrong"
assert exception["mechanism"]["type"] == "gcp"
assert not exception["mechanism"]["handled"]
assert envelope_items[1]["type"] == "transaction"
assert envelope_items[1]["contexts"]["trace"]["op"] == "function.gcp"
assert envelope_items[1]["transaction"].startswith("Google Cloud function")
assert envelope_items[1]["transaction"] in envelope_items[0]["request"]["url"]
def test_traces_sampler_gets_correct_values_in_sampling_context(
run_cloud_function, DictionaryContaining # noqa:N803
):
# TODO: There are some decent sized hacks below. For more context, see the
# long comment in the test of the same name in the AWS integration. The
# situations there and here aren't identical, but they're similar enough
# that solving one would probably solve both.
import inspect
_, return_value = run_cloud_function(
dedent(
"""
functionhandler = None
event = {
"type": "chase",
"chasers": ["Maisey", "Charlie"],
"num_squirrels": 2,
}
def cloud_function(functionhandler, event):
# this runs after the transaction has started, which means we
# can make assertions about traces_sampler
try:
traces_sampler.assert_any_call(
DictionaryContaining({
"gcp_env": DictionaryContaining({
"function_name": "chase_into_tree",
"function_region": "dogpark",
"function_project": "SquirrelChasing",
}),
"gcp_event": {
"type": "chase",
"chasers": ["Maisey", "Charlie"],
"num_squirrels": 2,
},
})
)
except AssertionError:
# catch the error and return it because the error itself will
# get swallowed by the SDK as an "internal exception"
return {"AssertionError raised": True,}
return {"AssertionError raised": False,}
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(inspect.getsource(DictionaryContaining))
+ dedent(
"""
os.environ["FUNCTION_NAME"] = "chase_into_tree"
os.environ["FUNCTION_REGION"] = "dogpark"
os.environ["GCP_PROJECT"] = "SquirrelChasing"
def _safe_is_equal(x, y):
# copied from conftest.py - see docstring and comments there
try:
is_equal = x.__eq__(y)
except AttributeError:
is_equal = NotImplemented
if is_equal == NotImplemented:
return x == y
return is_equal
traces_sampler = Mock(return_value=True)
init_sdk(
traces_sampler=traces_sampler,
)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
assert return_value["AssertionError raised"] is False
def test_error_has_new_trace_context_performance_enabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions when performance monitoring is enabled.
"""
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
sentry_sdk.capture_message("hi")
x = 3/0
return "3"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
(msg_event, error_event, transaction_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
)
def test_error_has_new_trace_context_performance_disabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions when performance monitoring is disabled.
"""
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
sentry_sdk.capture_message("hi")
x = 3/0
return "3"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=None), # this is the default, just added for clarity
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
(msg_event, error_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
)
def test_error_has_existing_trace_context_performance_enabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions
from the incoming 'sentry-trace' header when performance monitoring is enabled.
"""
trace_id = "471a43a4192642f0b136d5159a501701"
parent_span_id = "6e8f22c393e68f19"
parent_sampled = 1
sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
from collections import namedtuple
GCPEvent = namedtuple("GCPEvent", ["headers"])
event = GCPEvent(headers={"sentry-trace": "%s"})
def cloud_function(functionhandler, event):
sentry_sdk.capture_message("hi")
x = 3/0
return "3"
"""
% sentry_trace_header
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
(msg_event, error_event, transaction_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert "trace" in transaction_event["contexts"]
assert "trace_id" in transaction_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== transaction_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
def test_error_has_existing_trace_context_performance_disabled(run_cloud_function):
"""
Check if an 'trace' context is added to errros and transactions
from the incoming 'sentry-trace' header when performance monitoring is disabled.
"""
trace_id = "471a43a4192642f0b136d5159a501701"
parent_span_id = "6e8f22c393e68f19"
parent_sampled = 1
sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled)
envelope_items, _ = run_cloud_function(
dedent(
"""
functionhandler = None
from collections import namedtuple
GCPEvent = namedtuple("GCPEvent", ["headers"])
event = GCPEvent(headers={"sentry-trace": "%s"})
def cloud_function(functionhandler, event):
sentry_sdk.capture_message("hi")
x = 3/0
return "3"
"""
% sentry_trace_header
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=None), # this is the default, just added for clarity
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
(msg_event, error_event) = envelope_items
assert "trace" in msg_event["contexts"]
assert "trace_id" in msg_event["contexts"]["trace"]
assert "trace" in error_event["contexts"]
assert "trace_id" in error_event["contexts"]["trace"]
assert (
msg_event["contexts"]["trace"]["trace_id"]
== error_event["contexts"]["trace"]["trace_id"]
== "471a43a4192642f0b136d5159a501701"
)
def test_span_origin(run_cloud_function):
events, _ = run_cloud_function(
dedent(
"""
functionhandler = None
event = {}
def cloud_function(functionhandler, event):
return "test_string"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
)
)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.function.gcp"
sentry-python-2.18.0/tests/integrations/gql/ 0000775 0000000 0000000 00000000000 14712146540 0021104 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/gql/__init__.py 0000664 0000000 0000000 00000000052 14712146540 0023212 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("gql")
sentry-python-2.18.0/tests/integrations/gql/test_gql.py 0000664 0000000 0000000 00000006350 14712146540 0023304 0 ustar 00root root 0000000 0000000 import pytest
import responses
from gql import gql
from gql import Client
from gql.transport.exceptions import TransportQueryError
from gql.transport.requests import RequestsHTTPTransport
from sentry_sdk.integrations.gql import GQLIntegration
@responses.activate
def _execute_mock_query(response_json):
url = "http://example.com/graphql"
query_string = """
query Example {
example
}
"""
# Mock the GraphQL server response
responses.add(
method=responses.POST,
url=url,
json=response_json,
status=200,
)
transport = RequestsHTTPTransport(url=url)
client = Client(transport=transport)
query = gql(query_string)
return client.execute(query)
def _make_erroneous_query(capture_events):
"""
Make an erroneous GraphQL query, and assert that the error was reraised, that
exactly one event was recorded, and that the exception recorded was a
TransportQueryError. Then, return the event to allow further verifications.
"""
events = capture_events()
response_json = {"errors": ["something bad happened"]}
with pytest.raises(TransportQueryError):
_execute_mock_query(response_json)
assert (
len(events) == 1
), "the sdk captured %d events, but 1 event was expected" % len(events)
(event,) = events
(exception,) = event["exception"]["values"]
assert (
exception["type"] == "TransportQueryError"
), "%s was captured, but we expected a TransportQueryError" % exception(type)
assert "request" in event
return event
def test_gql_init(sentry_init):
"""
Integration test to ensure we can initialize the SDK with the GQL Integration
"""
sentry_init(integrations=[GQLIntegration()])
def test_real_gql_request_no_error(sentry_init, capture_events):
"""
Integration test verifying that the GQLIntegration works as expected with successful query.
"""
sentry_init(integrations=[GQLIntegration()])
events = capture_events()
response_data = {"example": "This is the example"}
response_json = {"data": response_data}
result = _execute_mock_query(response_json)
assert (
result == response_data
), "client.execute returned a different value from what it received from the server"
assert (
len(events) == 0
), "the sdk captured an event, even though the query was successful"
def test_real_gql_request_with_error_no_pii(sentry_init, capture_events):
"""
Integration test verifying that the GQLIntegration works as expected with query resulting
in a GraphQL error, and that PII is not sent.
"""
sentry_init(integrations=[GQLIntegration()])
event = _make_erroneous_query(capture_events)
assert "data" not in event["request"]
assert "response" not in event["contexts"]
def test_real_gql_request_with_error_with_pii(sentry_init, capture_events):
"""
Integration test verifying that the GQLIntegration works as expected with query resulting
in a GraphQL error, and that PII is not sent.
"""
sentry_init(integrations=[GQLIntegration()], send_default_pii=True)
event = _make_erroneous_query(capture_events)
assert "data" in event["request"]
assert "response" in event["contexts"]
sentry-python-2.18.0/tests/integrations/graphene/ 0000775 0000000 0000000 00000000000 14712146540 0022112 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/graphene/__init__.py 0000664 0000000 0000000 00000000153 14712146540 0024222 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("graphene")
pytest.importorskip("fastapi")
pytest.importorskip("flask")
sentry-python-2.18.0/tests/integrations/graphene/test_graphene.py 0000664 0000000 0000000 00000017572 14712146540 0025330 0 ustar 00root root 0000000 0000000 from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
from flask import Flask, request, jsonify
from graphene import ObjectType, String, Schema
from sentry_sdk.consts import OP
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.graphene import GrapheneIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
class Query(ObjectType):
hello = String(first_name=String(default_value="stranger"))
goodbye = String()
def resolve_hello(root, info, first_name): # noqa: N805
return "Hello {}!".format(first_name)
def resolve_goodbye(root, info): # noqa: N805
raise RuntimeError("oh no!")
def test_capture_request_if_available_and_send_pii_is_on_async(
sentry_init, capture_events
):
sentry_init(
send_default_pii=True,
integrations=[
GrapheneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = Schema(query=Query)
async_app = FastAPI()
@async_app.post("/graphql")
async def graphql_server_async(request: Request):
data = await request.json()
result = await schema.execute_async(data["query"])
return result.data
query = {"query": "query ErrorQuery {goodbye}"}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "graphene"
assert event["request"]["api_target"] == "graphql"
assert event["request"]["data"] == query
def test_capture_request_if_available_and_send_pii_is_on_sync(
sentry_init, capture_events
):
sentry_init(
send_default_pii=True,
integrations=[GrapheneIntegration(), FlaskIntegration()],
)
events = capture_events()
schema = Schema(query=Query)
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"])
return jsonify(result.data), 200
query = {"query": "query ErrorQuery {goodbye}"}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "graphene"
assert event["request"]["api_target"] == "graphql"
assert event["request"]["data"] == query
def test_do_not_capture_request_if_send_pii_is_off_async(sentry_init, capture_events):
sentry_init(
integrations=[
GrapheneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = Schema(query=Query)
async_app = FastAPI()
@async_app.post("/graphql")
async def graphql_server_async(request: Request):
data = await request.json()
result = await schema.execute_async(data["query"])
return result.data
query = {"query": "query ErrorQuery {goodbye}"}
client = TestClient(async_app)
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "graphene"
assert "data" not in event["request"]
assert "response" not in event["contexts"]
def test_do_not_capture_request_if_send_pii_is_off_sync(sentry_init, capture_events):
sentry_init(
integrations=[GrapheneIntegration(), FlaskIntegration()],
)
events = capture_events()
schema = Schema(query=Query)
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"])
return jsonify(result.data), 200
query = {"query": "query ErrorQuery {goodbye}"}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "graphene"
assert "data" not in event["request"]
assert "response" not in event["contexts"]
def test_no_event_if_no_errors_async(sentry_init, capture_events):
sentry_init(
integrations=[
GrapheneIntegration(),
FastApiIntegration(),
StarletteIntegration(),
],
)
events = capture_events()
schema = Schema(query=Query)
async_app = FastAPI()
@async_app.post("/graphql")
async def graphql_server_async(request: Request):
data = await request.json()
result = await schema.execute_async(data["query"])
return result.data
query = {
"query": "query GreetingQuery { hello }",
}
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=[
GrapheneIntegration(),
FlaskIntegration(),
],
)
events = capture_events()
schema = Schema(query=Query)
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"])
return jsonify(result.data), 200
query = {
"query": "query GreetingQuery { hello }",
}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 0
def test_graphql_span_holds_query_information(sentry_init, capture_events):
sentry_init(
integrations=[GrapheneIntegration(), FlaskIntegration()],
enable_tracing=True,
default_integrations=False,
)
events = capture_events()
schema = Schema(query=Query)
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"], operation_name=data.get("operationName"))
return jsonify(result.data), 200
query = {
"query": "query GreetingQuery { hello }",
"operationName": "GreetingQuery",
}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert len(event["spans"]) == 1
(span,) = event["spans"]
assert span["op"] == OP.GRAPHQL_QUERY
assert span["description"] == query["operationName"]
assert span["data"]["graphql.document"] == query["query"]
assert span["data"]["graphql.operation.name"] == query["operationName"]
assert span["data"]["graphql.operation.type"] == "query"
def test_breadcrumbs_hold_query_information_on_error(sentry_init, capture_events):
sentry_init(
integrations=[
GrapheneIntegration(),
],
default_integrations=False,
)
events = capture_events()
schema = Schema(query=Query)
sync_app = Flask(__name__)
@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"], operation_name=data.get("operationName"))
return jsonify(result.data), 200
query = {
"query": "query ErrorQuery { goodbye }",
"operationName": "ErrorQuery",
}
client = sync_app.test_client()
client.post("/graphql", json=query)
assert len(events) == 1
(event,) = events
assert len(event["breadcrumbs"]) == 1
breadcrumbs = event["breadcrumbs"]["values"]
assert len(breadcrumbs) == 1
(breadcrumb,) = breadcrumbs
assert breadcrumb["category"] == "graphql.operation"
assert breadcrumb["data"]["operation_name"] == query["operationName"]
assert breadcrumb["data"]["operation_type"] == "query"
assert breadcrumb["type"] == "default"
sentry-python-2.18.0/tests/integrations/grpc/ 0000775 0000000 0000000 00000000000 14712146540 0021254 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/grpc/__init__.py 0000664 0000000 0000000 00000000261 14712146540 0023364 0 ustar 00root root 0000000 0000000 import sys
from pathlib import Path
import pytest
# For imports inside gRPC autogenerated code to work
sys.path.append(str(Path(__file__).parent))
pytest.importorskip("grpc")
sentry-python-2.18.0/tests/integrations/grpc/compile_test_services.sh 0000775 0000000 0000000 00000000623 14712146540 0026206 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
# Run this script from the project root to generate the python code
TARGET_PATH=./tests/integrations/grpc
# Create python file
python -m grpc_tools.protoc \
--proto_path=$TARGET_PATH/protos/ \
--python_out=$TARGET_PATH/ \
--pyi_out=$TARGET_PATH/ \
--grpc_python_out=$TARGET_PATH/ \
$TARGET_PATH/protos/grpc_test_service.proto
echo Code generation successfull
sentry-python-2.18.0/tests/integrations/grpc/grpc_test_service_pb2.py 0000664 0000000 0000000 00000003110 14712146540 0026076 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: grpc_test_service.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17grpc_test_service.proto\x12\x10grpc_test_server\"\x1f\n\x0fgRPCTestMessage\x12\x0c\n\x04text\x18\x01 \x01(\t2\xf8\x02\n\x0fgRPCTestService\x12Q\n\tTestServe\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage\x12Y\n\x0fTestUnaryStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage0\x01\x12\\\n\x10TestStreamStream\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage(\x01\x30\x01\x12Y\n\x0fTestStreamUnary\x12!.grpc_test_server.gRPCTestMessage\x1a!.grpc_test_server.gRPCTestMessage(\x01\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpc_test_service_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_globals['_GRPCTESTMESSAGE']._serialized_start=45
_globals['_GRPCTESTMESSAGE']._serialized_end=76
_globals['_GRPCTESTSERVICE']._serialized_start=79
_globals['_GRPCTESTSERVICE']._serialized_end=455
# @@protoc_insertion_point(module_scope)
sentry-python-2.18.0/tests/integrations/grpc/grpc_test_service_pb2.pyi 0000664 0000000 0000000 00000000605 14712146540 0026255 0 ustar 00root root 0000000 0000000 from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional
DESCRIPTOR: _descriptor.FileDescriptor
class gRPCTestMessage(_message.Message):
__slots__ = ["text"]
TEXT_FIELD_NUMBER: _ClassVar[int]
text: str
def __init__(self, text: _Optional[str] = ...) -> None: ...
sentry-python-2.18.0/tests/integrations/grpc/grpc_test_service_pb2_grpc.py 0000664 0000000 0000000 00000016605 14712146540 0027126 0 ustar 00root root 0000000 0000000 # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import grpc_test_service_pb2 as grpc__test__service__pb2
class gRPCTestServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.TestServe = channel.unary_unary(
'/grpc_test_server.gRPCTestService/TestServe',
request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
)
self.TestUnaryStream = channel.unary_stream(
'/grpc_test_server.gRPCTestService/TestUnaryStream',
request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
)
self.TestStreamStream = channel.stream_stream(
'/grpc_test_server.gRPCTestService/TestStreamStream',
request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
)
self.TestStreamUnary = channel.stream_unary(
'/grpc_test_server.gRPCTestService/TestStreamUnary',
request_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
response_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
)
class gRPCTestServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def TestServe(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TestUnaryStream(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TestStreamStream(self, request_iterator, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def TestStreamUnary(self, request_iterator, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_gRPCTestServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'TestServe': grpc.unary_unary_rpc_method_handler(
servicer.TestServe,
request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
),
'TestUnaryStream': grpc.unary_stream_rpc_method_handler(
servicer.TestUnaryStream,
request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
),
'TestStreamStream': grpc.stream_stream_rpc_method_handler(
servicer.TestStreamStream,
request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
),
'TestStreamUnary': grpc.stream_unary_rpc_method_handler(
servicer.TestStreamUnary,
request_deserializer=grpc__test__service__pb2.gRPCTestMessage.FromString,
response_serializer=grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'grpc_test_server.gRPCTestService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class gRPCTestService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def TestServe(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/grpc_test_server.gRPCTestService/TestServe',
grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
grpc__test__service__pb2.gRPCTestMessage.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def TestUnaryStream(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(request, target, '/grpc_test_server.gRPCTestService/TestUnaryStream',
grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
grpc__test__service__pb2.gRPCTestMessage.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def TestStreamStream(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(request_iterator, target, '/grpc_test_server.gRPCTestService/TestStreamStream',
grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
grpc__test__service__pb2.gRPCTestMessage.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@staticmethod
def TestStreamUnary(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_unary(request_iterator, target, '/grpc_test_server.gRPCTestService/TestStreamUnary',
grpc__test__service__pb2.gRPCTestMessage.SerializeToString,
grpc__test__service__pb2.gRPCTestMessage.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
sentry-python-2.18.0/tests/integrations/grpc/protos/ 0000775 0000000 0000000 00000000000 14712146540 0022602 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/grpc/protos/grpc_test_service.proto 0000664 0000000 0000000 00000000631 14712146540 0027401 0 ustar 00root root 0000000 0000000 syntax = "proto3";
package grpc_test_server;
service gRPCTestService{
rpc TestServe(gRPCTestMessage) returns (gRPCTestMessage);
rpc TestUnaryStream(gRPCTestMessage) returns (stream gRPCTestMessage);
rpc TestStreamStream(stream gRPCTestMessage) returns (stream gRPCTestMessage);
rpc TestStreamUnary(stream gRPCTestMessage) returns (gRPCTestMessage);
}
message gRPCTestMessage {
string text = 1;
}
sentry-python-2.18.0/tests/integrations/grpc/test_grpc.py 0000664 0000000 0000000 00000026627 14712146540 0023635 0 ustar 00root root 0000000 0000000 import os
import grpc
import pytest
from concurrent import futures
from typing import List, Optional
from unittest.mock import Mock
from sentry_sdk import start_span, start_transaction
from sentry_sdk.consts import OP
from sentry_sdk.integrations.grpc import GRPCIntegration
from tests.conftest import ApproxDict
from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage
from tests.integrations.grpc.grpc_test_service_pb2_grpc import (
add_gRPCTestServiceServicer_to_server,
gRPCTestServiceServicer,
gRPCTestServiceStub,
)
PORT = 50051
PORT += os.getpid() % 100 # avoid port conflicts when running tests in parallel
def _set_up(interceptors: Optional[List[grpc.ServerInterceptor]] = None):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=2),
interceptors=interceptors,
)
add_gRPCTestServiceServicer_to_server(TestService(), server)
server.add_insecure_port("[::]:{}".format(PORT))
server.start()
return server
def _tear_down(server: grpc.Server):
server.stop(None)
@pytest.mark.forked
def test_grpc_server_starts_transaction(sentry_init, capture_events_forksafe):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
events.write_file.close()
event = events.read_event()
span = event["spans"][0]
assert event["type"] == "transaction"
assert event["transaction_info"] == {
"source": "custom",
}
assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER
assert span["op"] == "test"
@pytest.mark.forked
def test_grpc_server_other_interceptors(sentry_init, capture_events_forksafe):
"""Ensure compatibility with additional server interceptors."""
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
mock_intercept = lambda continuation, handler_call_details: continuation(
handler_call_details
)
mock_interceptor = Mock()
mock_interceptor.intercept_service.side_effect = mock_intercept
server = _set_up(interceptors=[mock_interceptor])
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
mock_interceptor.intercept_service.assert_called_once()
events.write_file.close()
event = events.read_event()
span = event["spans"][0]
assert event["type"] == "transaction"
assert event["transaction_info"] == {
"source": "custom",
}
assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER
assert span["op"] == "test"
@pytest.mark.forked
def test_grpc_server_continues_transaction(sentry_init, capture_events_forksafe):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction() as transaction:
metadata = (
(
"baggage",
"sentry-trace_id={trace_id},sentry-environment=test,"
"sentry-transaction=test-transaction,sentry-sample_rate=1.0".format(
trace_id=transaction.trace_id
),
),
(
"sentry-trace",
"{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=transaction.span_id,
sampled=1,
),
),
)
stub.TestServe(gRPCTestMessage(text="test"), metadata=metadata)
_tear_down(server=server)
events.write_file.close()
event = events.read_event()
span = event["spans"][0]
assert event["type"] == "transaction"
assert event["transaction_info"] == {
"source": "custom",
}
assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER
assert event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert span["op"] == "test"
@pytest.mark.forked
def test_grpc_client_starts_span(sentry_init, capture_events_forksafe):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction():
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
events.write_file.close()
events.read_event()
local_transaction = events.read_event()
span = local_transaction["spans"][0]
assert len(local_transaction["spans"]) == 1
assert span["op"] == OP.GRPC_CLIENT
assert (
span["description"]
== "unary unary call to /grpc_test_server.gRPCTestService/TestServe"
)
assert span["data"] == ApproxDict(
{
"type": "unary unary",
"method": "/grpc_test_server.gRPCTestService/TestServe",
"code": "OK",
}
)
@pytest.mark.forked
def test_grpc_client_unary_stream_starts_span(sentry_init, capture_events_forksafe):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction():
[el for el in stub.TestUnaryStream(gRPCTestMessage(text="test"))]
_tear_down(server=server)
events.write_file.close()
local_transaction = events.read_event()
span = local_transaction["spans"][0]
assert len(local_transaction["spans"]) == 1
assert span["op"] == OP.GRPC_CLIENT
assert (
span["description"]
== "unary stream call to /grpc_test_server.gRPCTestService/TestUnaryStream"
)
assert span["data"] == ApproxDict(
{
"type": "unary stream",
"method": "/grpc_test_server.gRPCTestService/TestUnaryStream",
}
)
# using unittest.mock.Mock not possible because grpc verifies
# that the interceptor is of the correct type
class MockClientInterceptor(grpc.UnaryUnaryClientInterceptor):
call_counter = 0
def intercept_unary_unary(self, continuation, client_call_details, request):
self.__class__.call_counter += 1
return continuation(client_call_details, request)
@pytest.mark.forked
def test_grpc_client_other_interceptor(sentry_init, capture_events_forksafe):
"""Ensure compatibility with additional client interceptors."""
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
channel = grpc.intercept_channel(channel, MockClientInterceptor())
stub = gRPCTestServiceStub(channel)
with start_transaction():
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
assert MockClientInterceptor.call_counter == 1
events.write_file.close()
events.read_event()
local_transaction = events.read_event()
span = local_transaction["spans"][0]
assert len(local_transaction["spans"]) == 1
assert span["op"] == OP.GRPC_CLIENT
assert (
span["description"]
== "unary unary call to /grpc_test_server.gRPCTestService/TestServe"
)
assert span["data"] == ApproxDict(
{
"type": "unary unary",
"method": "/grpc_test_server.gRPCTestService/TestServe",
"code": "OK",
}
)
@pytest.mark.forked
def test_grpc_client_and_servers_interceptors_integration(
sentry_init, capture_events_forksafe
):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction():
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
events.write_file.close()
server_transaction = events.read_event()
local_transaction = events.read_event()
assert (
server_transaction["contexts"]["trace"]["trace_id"]
== local_transaction["contexts"]["trace"]["trace_id"]
)
@pytest.mark.forked
def test_stream_stream(sentry_init):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
response_iterator = stub.TestStreamStream(iter((gRPCTestMessage(text="test"),)))
for response in response_iterator:
assert response.text == "test"
_tear_down(server=server)
@pytest.mark.forked
def test_stream_unary(sentry_init):
"""
Test to verify stream-stream works.
Tracing not supported for it yet.
"""
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
response = stub.TestStreamUnary(iter((gRPCTestMessage(text="test"),)))
assert response.text == "test"
_tear_down(server=server)
@pytest.mark.forked
def test_span_origin(sentry_init, capture_events_forksafe):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
events = capture_events_forksafe()
server = _set_up()
with grpc.insecure_channel("localhost:{}".format(PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction(name="custom_transaction"):
stub.TestServe(gRPCTestMessage(text="test"))
_tear_down(server=server)
events.write_file.close()
transaction_from_integration = events.read_event()
custom_transaction = events.read_event()
assert (
transaction_from_integration["contexts"]["trace"]["origin"] == "auto.grpc.grpc"
)
assert (
transaction_from_integration["spans"][0]["origin"]
== "auto.grpc.grpc.TestService"
) # manually created in TestService, not the instrumentation
assert custom_transaction["contexts"]["trace"]["origin"] == "manual"
assert custom_transaction["spans"][0]["origin"] == "auto.grpc.grpc"
class TestService(gRPCTestServiceServicer):
events = []
@staticmethod
def TestServe(request, context): # noqa: N802
with start_span(
op="test",
name="test",
origin="auto.grpc.grpc.TestService",
):
pass
return gRPCTestMessage(text=request.text)
@staticmethod
def TestUnaryStream(request, context): # noqa: N802
for _ in range(3):
yield gRPCTestMessage(text=request.text)
@staticmethod
def TestStreamStream(request, context): # noqa: N802
for r in request:
yield r
@staticmethod
def TestStreamUnary(request, context): # noqa: N802
requests = [r for r in request]
return requests.pop()
sentry-python-2.18.0/tests/integrations/grpc/test_grpc_aio.py 0000664 0000000 0000000 00000023326 14712146540 0024456 0 ustar 00root root 0000000 0000000 import asyncio
import os
import grpc
import pytest
import pytest_asyncio
import sentry_sdk
from sentry_sdk import start_span, start_transaction
from sentry_sdk.consts import OP
from sentry_sdk.integrations.grpc import GRPCIntegration
from tests.conftest import ApproxDict
from tests.integrations.grpc.grpc_test_service_pb2 import gRPCTestMessage
from tests.integrations.grpc.grpc_test_service_pb2_grpc import (
add_gRPCTestServiceServicer_to_server,
gRPCTestServiceServicer,
gRPCTestServiceStub,
)
AIO_PORT = 50052
AIO_PORT += os.getpid() % 100 # avoid port conflicts when running tests in parallel
@pytest.fixture(scope="function")
def event_loop(request):
"""Create an instance of the default event loop for each test case."""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="function")
async def grpc_server(sentry_init, event_loop):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
server = grpc.aio.server()
server.add_insecure_port("[::]:{}".format(AIO_PORT))
add_gRPCTestServiceServicer_to_server(TestService, server)
await event_loop.create_task(server.start())
try:
yield server
finally:
await server.stop(None)
@pytest.mark.asyncio
async def test_noop_for_unimplemented_method(event_loop, sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[GRPCIntegration()])
server = grpc.aio.server()
server.add_insecure_port("[::]:{}".format(AIO_PORT))
await event_loop.create_task(server.start())
events = capture_events()
try:
async with grpc.aio.insecure_channel(
"localhost:{}".format(AIO_PORT)
) as channel:
stub = gRPCTestServiceStub(channel)
with pytest.raises(grpc.RpcError) as exc:
await stub.TestServe(gRPCTestMessage(text="test"))
assert exc.value.details() == "Method not found!"
finally:
await server.stop(None)
assert not events
@pytest.mark.asyncio
async def test_grpc_server_starts_transaction(grpc_server, capture_events):
events = capture_events()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
await stub.TestServe(gRPCTestMessage(text="test"))
(event,) = events
span = event["spans"][0]
assert event["type"] == "transaction"
assert event["transaction_info"] == {
"source": "custom",
}
assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER
assert span["op"] == "test"
@pytest.mark.asyncio
async def test_grpc_server_continues_transaction(grpc_server, capture_events):
events = capture_events()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with sentry_sdk.start_transaction() as transaction:
metadata = (
(
"baggage",
"sentry-trace_id={trace_id},sentry-environment=test,"
"sentry-transaction=test-transaction,sentry-sample_rate=1.0".format(
trace_id=transaction.trace_id
),
),
(
"sentry-trace",
"{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=transaction.span_id,
sampled=1,
),
),
)
await stub.TestServe(gRPCTestMessage(text="test"), metadata=metadata)
(event, _) = events
span = event["spans"][0]
assert event["type"] == "transaction"
assert event["transaction_info"] == {
"source": "custom",
}
assert event["contexts"]["trace"]["op"] == OP.GRPC_SERVER
assert event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert span["op"] == "test"
@pytest.mark.asyncio
async def test_grpc_server_exception(grpc_server, capture_events):
events = capture_events()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
try:
await stub.TestServe(gRPCTestMessage(text="exception"))
raise AssertionError()
except Exception:
pass
(event, _) = events
assert event["exception"]["values"][0]["type"] == "TestService.TestException"
assert event["exception"]["values"][0]["value"] == "test"
assert event["exception"]["values"][0]["mechanism"]["handled"] is False
assert event["exception"]["values"][0]["mechanism"]["type"] == "grpc"
@pytest.mark.asyncio
async def test_grpc_server_abort(grpc_server, capture_events):
events = capture_events()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
try:
await stub.TestServe(gRPCTestMessage(text="abort"))
raise AssertionError()
except Exception:
pass
assert len(events) == 1
@pytest.mark.asyncio
async def test_grpc_client_starts_span(grpc_server, capture_events_forksafe):
events = capture_events_forksafe()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction():
await stub.TestServe(gRPCTestMessage(text="test"))
events.write_file.close()
events.read_event()
local_transaction = events.read_event()
span = local_transaction["spans"][0]
assert len(local_transaction["spans"]) == 1
assert span["op"] == OP.GRPC_CLIENT
assert (
span["description"]
== "unary unary call to /grpc_test_server.gRPCTestService/TestServe"
)
assert span["data"] == ApproxDict(
{
"type": "unary unary",
"method": "/grpc_test_server.gRPCTestService/TestServe",
"code": "OK",
}
)
@pytest.mark.asyncio
async def test_grpc_client_unary_stream_starts_span(
grpc_server, capture_events_forksafe
):
events = capture_events_forksafe()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction():
response = stub.TestUnaryStream(gRPCTestMessage(text="test"))
[_ async for _ in response]
events.write_file.close()
local_transaction = events.read_event()
span = local_transaction["spans"][0]
assert len(local_transaction["spans"]) == 1
assert span["op"] == OP.GRPC_CLIENT
assert (
span["description"]
== "unary stream call to /grpc_test_server.gRPCTestService/TestUnaryStream"
)
assert span["data"] == ApproxDict(
{
"type": "unary stream",
"method": "/grpc_test_server.gRPCTestService/TestUnaryStream",
}
)
@pytest.mark.asyncio
async def test_stream_stream(grpc_server):
"""
Test to verify stream-stream works.
Tracing not supported for it yet.
"""
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
response = stub.TestStreamStream((gRPCTestMessage(text="test"),))
async for r in response:
assert r.text == "test"
@pytest.mark.asyncio
async def test_stream_unary(grpc_server):
"""
Test to verify stream-stream works.
Tracing not supported for it yet.
"""
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
response = await stub.TestStreamUnary((gRPCTestMessage(text="test"),))
assert response.text == "test"
@pytest.mark.asyncio
async def test_span_origin(grpc_server, capture_events_forksafe):
events = capture_events_forksafe()
async with grpc.aio.insecure_channel("localhost:{}".format(AIO_PORT)) as channel:
stub = gRPCTestServiceStub(channel)
with start_transaction(name="custom_transaction"):
await stub.TestServe(gRPCTestMessage(text="test"))
events.write_file.close()
transaction_from_integration = events.read_event()
custom_transaction = events.read_event()
assert (
transaction_from_integration["contexts"]["trace"]["origin"] == "auto.grpc.grpc"
)
assert (
transaction_from_integration["spans"][0]["origin"]
== "auto.grpc.grpc.TestService.aio"
) # manually created in TestService, not the instrumentation
assert custom_transaction["contexts"]["trace"]["origin"] == "manual"
assert custom_transaction["spans"][0]["origin"] == "auto.grpc.grpc"
class TestService(gRPCTestServiceServicer):
class TestException(Exception):
__test__ = False
def __init__(self):
super().__init__("test")
@classmethod
async def TestServe(cls, request, context): # noqa: N802
with start_span(
op="test",
name="test",
origin="auto.grpc.grpc.TestService.aio",
):
pass
if request.text == "exception":
raise cls.TestException()
if request.text == "abort":
await context.abort(grpc.StatusCode.ABORTED)
return gRPCTestMessage(text=request.text)
@classmethod
async def TestUnaryStream(cls, request, context): # noqa: N802
for _ in range(3):
yield gRPCTestMessage(text=request.text)
@classmethod
async def TestStreamStream(cls, request, context): # noqa: N802
async for r in request:
yield r
@classmethod
async def TestStreamUnary(cls, request, context): # noqa: N802
requests = [r async for r in request]
return requests.pop()
sentry-python-2.18.0/tests/integrations/httpx/ 0000775 0000000 0000000 00000000000 14712146540 0021470 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/httpx/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0023600 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("httpx")
sentry-python-2.18.0/tests/integrations/httpx/test_httpx.py 0000664 0000000 0000000 00000023716 14712146540 0024261 0 ustar 00root root 0000000 0000000 import asyncio
from unittest import mock
import httpx
import pytest
import responses
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import MATCH_ALL, SPANDATA
from sentry_sdk.integrations.httpx import HttpxIntegration
from tests.conftest import ApproxDict
@pytest.mark.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_crumb_capture_and_hint(sentry_init, capture_events, httpx_client):
def before_breadcrumb(crumb, hint):
crumb["data"]["extra"] = "foo"
return crumb
sentry_init(integrations=[HttpxIntegration()], before_breadcrumb=before_breadcrumb)
url = "http://example.com/"
responses.add(responses.GET, url, status=200)
with start_transaction():
events = capture_events()
if asyncio.iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
else:
response = httpx_client.get(url)
assert response.status_code == 200
capture_message("Testing!")
(event,) = events
crumb = event["breadcrumbs"]["values"][0]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == ApproxDict(
{
"url": url,
SPANDATA.HTTP_METHOD: "GET",
SPANDATA.HTTP_FRAGMENT: "",
SPANDATA.HTTP_QUERY: "",
SPANDATA.HTTP_STATUS_CODE: 200,
"reason": "OK",
"extra": "foo",
}
)
@pytest.mark.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_outgoing_trace_headers(sentry_init, httpx_client):
sentry_init(traces_sample_rate=1.0, integrations=[HttpxIntegration()])
url = "http://example.com/"
responses.add(responses.GET, url, status=200)
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
trace_id="01234567890123456789012345678901",
) as transaction:
if asyncio.iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
else:
response = httpx_client.get(url)
request_span = transaction._span_recorder.spans[-1]
assert response.request.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.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_outgoing_trace_headers_append_to_baggage(sentry_init, httpx_client):
sentry_init(
traces_sample_rate=1.0,
integrations=[HttpxIntegration()],
release="d08ebdb9309e1b004c6f52202de58a09c2268e42",
)
url = "http://example.com/"
responses.add(responses.GET, url, status=200)
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
trace_id="01234567890123456789012345678901",
) as transaction:
if asyncio.iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url, headers={"baGGage": "custom=data"})
)
else:
response = httpx_client.get(url, headers={"baGGage": "custom=data"})
request_span = transaction._span_recorder.spans[-1]
assert response.request.headers[
"sentry-trace"
] == "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,
)
assert (
response.request.headers["baggage"]
== "custom=data,sentry-trace_id=01234567890123456789012345678901,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
)
@pytest.mark.parametrize(
"httpx_client,trace_propagation_targets,url,trace_propagated",
[
[
httpx.Client(),
None,
"https://example.com/",
False,
],
[
httpx.Client(),
[],
"https://example.com/",
False,
],
[
httpx.Client(),
[MATCH_ALL],
"https://example.com/",
True,
],
[
httpx.Client(),
["https://example.com/"],
"https://example.com/",
True,
],
[
httpx.Client(),
["https://example.com/"],
"https://example.com",
False,
],
[
httpx.Client(),
["https://example.com"],
"https://example.com",
True,
],
[
httpx.Client(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://example.net",
False,
],
[
httpx.Client(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://good.example.net",
True,
],
[
httpx.Client(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://good.example.net/some/thing",
True,
],
[
httpx.AsyncClient(),
None,
"https://example.com/",
False,
],
[
httpx.AsyncClient(),
[],
"https://example.com/",
False,
],
[
httpx.AsyncClient(),
[MATCH_ALL],
"https://example.com/",
True,
],
[
httpx.AsyncClient(),
["https://example.com/"],
"https://example.com/",
True,
],
[
httpx.AsyncClient(),
["https://example.com/"],
"https://example.com",
False,
],
[
httpx.AsyncClient(),
["https://example.com"],
"https://example.com",
True,
],
[
httpx.AsyncClient(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://example.net",
False,
],
[
httpx.AsyncClient(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://good.example.net",
True,
],
[
httpx.AsyncClient(),
["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"],
"https://good.example.net/some/thing",
True,
],
],
)
def test_option_trace_propagation_targets(
sentry_init,
httpx_client,
httpx_mock, # this comes from pytest-httpx
trace_propagation_targets,
url,
trace_propagated,
):
httpx_mock.add_response()
sentry_init(
release="test",
trace_propagation_targets=trace_propagation_targets,
traces_sample_rate=1.0,
integrations=[HttpxIntegration()],
)
with sentry_sdk.start_transaction(): # Must be in a transaction to propagate headers
if asyncio.iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
request_headers = httpx_mock.get_request().headers
if trace_propagated:
assert "sentry-trace" in request_headers
else:
assert "sentry-trace" not in request_headers
def test_do_not_propagate_outside_transaction(sentry_init, httpx_mock):
httpx_mock.add_response()
sentry_init(
traces_sample_rate=1.0,
trace_propagation_targets=[MATCH_ALL],
integrations=[HttpxIntegration()],
)
httpx_client = httpx.Client()
httpx_client.get("http://example.com/")
request_headers = httpx_mock.get_request().headers
assert "sentry-trace" not in request_headers
@pytest.mark.tests_internal_exceptions
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
sentry_init(integrations=[HttpxIntegration()])
httpx_client = httpx.Client()
url = "http://example.com"
responses.add(responses.GET, url, status=200)
events = capture_events()
with mock.patch(
"sentry_sdk.integrations.httpx.parse_url",
side_effect=ValueError,
):
response = httpx_client.get(url)
assert response.status_code == 200
capture_message("Testing!")
(event,) = events
assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict(
{
SPANDATA.HTTP_METHOD: "GET",
SPANDATA.HTTP_STATUS_CODE: 200,
"reason": "OK",
# no url related data
}
)
assert "url" not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"]
@pytest.mark.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_span_origin(sentry_init, capture_events, httpx_client):
sentry_init(
integrations=[HttpxIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
url = "http://example.com/"
responses.add(responses.GET, url, status=200)
with start_transaction(name="test_transaction"):
if asyncio.iscoroutinefunction(httpx_client.get):
asyncio.get_event_loop().run_until_complete(httpx_client.get(url))
else:
httpx_client.get(url)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.http.httpx"
sentry-python-2.18.0/tests/integrations/huey/ 0000775 0000000 0000000 00000000000 14712146540 0021273 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/huey/__init__.py 0000664 0000000 0000000 00000000053 14712146540 0023402 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("huey")
sentry-python-2.18.0/tests/integrations/huey/test_huey.py 0000664 0000000 0000000 00000013177 14712146540 0023667 0 ustar 00root root 0000000 0000000 import pytest
from decimal import DivisionByZero
from sentry_sdk import start_transaction
from sentry_sdk.integrations.huey import HueyIntegration
from sentry_sdk.utils import parse_version
from huey import __version__ as HUEY_VERSION
from huey.api import MemoryHuey, Result
from huey.exceptions import RetryTask
HUEY_VERSION = parse_version(HUEY_VERSION)
@pytest.fixture
def init_huey(sentry_init):
def inner():
sentry_init(
integrations=[HueyIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
return MemoryHuey(name="sentry_sdk")
return inner
@pytest.fixture(autouse=True)
def flush_huey_tasks(init_huey):
huey = init_huey()
huey.flush()
def execute_huey_task(huey, func, *args, **kwargs):
exceptions = kwargs.pop("exceptions", None)
result = func(*args, **kwargs)
task = huey.dequeue()
if exceptions is not None:
try:
huey.execute(task)
except exceptions:
pass
else:
huey.execute(task)
return result
def test_task_result(init_huey):
huey = init_huey()
@huey.task()
def increase(num):
return num + 1
result = increase(3)
assert isinstance(result, Result)
assert len(huey) == 1
task = huey.dequeue()
assert huey.execute(task) == 4
assert result.get() == 4
@pytest.mark.parametrize("task_fails", [True, False], ids=["error", "success"])
def test_task_transaction(capture_events, init_huey, task_fails):
huey = init_huey()
@huey.task()
def division(a, b):
return a / b
events = capture_events()
execute_huey_task(
huey, division, 1, int(not task_fails), exceptions=(DivisionByZero,)
)
if task_fails:
error_event = events.pop(0)
assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert error_event["exception"]["values"][0]["mechanism"]["type"] == "huey"
(event,) = events
assert event["type"] == "transaction"
assert event["transaction"] == "division"
assert event["transaction_info"] == {"source": "task"}
if task_fails:
assert event["contexts"]["trace"]["status"] == "internal_error"
else:
assert event["contexts"]["trace"]["status"] == "ok"
assert "huey_task_id" in event["tags"]
assert "huey_task_retry" in event["tags"]
def test_task_retry(capture_events, init_huey):
huey = init_huey()
context = {"retry": True}
@huey.task()
def retry_task(context):
if context["retry"]:
context["retry"] = False
raise RetryTask()
events = capture_events()
result = execute_huey_task(huey, retry_task, context)
(event,) = events
assert event["transaction"] == "retry_task"
assert event["tags"]["huey_task_id"] == result.task.id
assert len(huey) == 1
task = huey.dequeue()
huey.execute(task)
(event, _) = events
assert event["transaction"] == "retry_task"
assert event["tags"]["huey_task_id"] == result.task.id
assert len(huey) == 0
@pytest.mark.parametrize("lock_name", ["lock.a", "lock.b"], ids=["locked", "unlocked"])
@pytest.mark.skipif(HUEY_VERSION < (2, 5), reason="is_locked was added in 2.5")
def test_task_lock(capture_events, init_huey, lock_name):
huey = init_huey()
task_lock_name = "lock.a"
should_be_locked = task_lock_name == lock_name
@huey.task()
@huey.lock_task(task_lock_name)
def maybe_locked_task():
pass
events = capture_events()
with huey.lock_task(lock_name):
assert huey.is_locked(task_lock_name) == should_be_locked
result = execute_huey_task(huey, maybe_locked_task)
(event,) = events
assert event["transaction"] == "maybe_locked_task"
assert event["tags"]["huey_task_id"] == result.task.id
assert (
event["contexts"]["trace"]["status"] == "aborted" if should_be_locked else "ok"
)
assert len(huey) == 0
def test_huey_enqueue(init_huey, capture_events):
huey = init_huey()
@huey.task(name="different_task_name")
def dummy_task():
pass
events = capture_events()
with start_transaction() as transaction:
dummy_task()
(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.huey"
assert event["spans"][0]["description"] == "different_task_name"
def test_huey_propagate_trace(init_huey, capture_events):
huey = init_huey()
events = capture_events()
@huey.task()
def propagated_trace_task():
pass
with start_transaction() as outer_transaction:
execute_huey_task(huey, propagated_trace_task)
assert (
events[0]["transaction"] == "propagated_trace_task"
) # the "inner" transaction
assert events[0]["contexts"]["trace"]["trace_id"] == outer_transaction.trace_id
def test_span_origin_producer(init_huey, capture_events):
huey = init_huey()
@huey.task(name="different_task_name")
def dummy_task():
pass
events = capture_events()
with start_transaction():
dummy_task()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.queue.huey"
def test_span_origin_consumer(init_huey, capture_events):
huey = init_huey()
events = capture_events()
@huey.task()
def propagated_trace_task():
pass
execute_huey_task(huey, propagated_trace_task)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.queue.huey"
sentry-python-2.18.0/tests/integrations/huggingface_hub/ 0000775 0000000 0000000 00000000000 14712146540 0023426 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/huggingface_hub/__init__.py 0000664 0000000 0000000 00000000066 14712146540 0025541 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("huggingface_hub")
sentry-python-2.18.0/tests/integrations/huggingface_hub/test_huggingface_hub.py 0000664 0000000 0000000 00000012264 14712146540 0030151 0 ustar 00root root 0000000 0000000 import itertools
import pytest
from huggingface_hub import (
InferenceClient,
)
from huggingface_hub.errors import OverloadedError
from sentry_sdk import start_transaction
from sentry_sdk.integrations.huggingface_hub import HuggingfaceHubIntegration
from unittest import mock # python 3.3 and above
@pytest.mark.parametrize(
"send_default_pii, include_prompts, details_arg",
itertools.product([True, False], repeat=3),
)
def test_nonstreaming_chat_completion(
sentry_init, capture_events, send_default_pii, include_prompts, details_arg
):
sentry_init(
integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = InferenceClient("some-model")
if details_arg:
client.post = mock.Mock(
return_value=b"""[{
"generated_text": "the model response",
"details": {
"finish_reason": "length",
"generated_tokens": 10,
"prefill": [],
"tokens": []
}
}]"""
)
else:
client.post = mock.Mock(
return_value=b'[{"generated_text": "the model response"}]'
)
with start_transaction(name="huggingface_hub tx"):
response = client.text_generation(
prompt="hello",
details=details_arg,
stream=False,
)
if details_arg:
assert response.generated_text == "the model response"
else:
assert response == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.huggingface_hub"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]
assert "the model response" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
if details_arg:
assert span["measurements"]["ai_total_tokens_used"]["value"] == 10
@pytest.mark.parametrize(
"send_default_pii, include_prompts, details_arg",
itertools.product([True, False], repeat=3),
)
def test_streaming_chat_completion(
sentry_init, capture_events, send_default_pii, include_prompts, details_arg
):
sentry_init(
integrations=[HuggingfaceHubIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = InferenceClient("some-model")
client.post = mock.Mock(
return_value=[
b"""data:{
"token":{"id":1, "special": false, "text": "the model "}
}""",
b"""data:{
"token":{"id":2, "special": false, "text": "response"},
"details":{"finish_reason": "length", "generated_tokens": 10, "seed": 0}
}""",
]
)
with start_transaction(name="huggingface_hub tx"):
response = list(
client.text_generation(
prompt="hello",
details=details_arg,
stream=True,
)
)
assert len(response) == 2
print(response)
if details_arg:
assert response[0].token.text + response[1].token.text == "the model response"
else:
assert response[0] + response[1] == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.huggingface_hub"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]
assert "the model response" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
if details_arg:
assert span["measurements"]["ai_total_tokens_used"]["value"] == 10
def test_bad_chat_completion(sentry_init, capture_events):
sentry_init(integrations=[HuggingfaceHubIntegration()], traces_sample_rate=1.0)
events = capture_events()
client = InferenceClient("some-model")
client.post = mock.Mock(side_effect=OverloadedError("The server is overloaded"))
with pytest.raises(OverloadedError):
client.text_generation(prompt="hello")
(event,) = events
assert event["level"] == "error"
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[HuggingfaceHubIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = InferenceClient("some-model")
client.post = mock.Mock(
return_value=[
b"""data:{
"token":{"id":1, "special": false, "text": "the model "}
}""",
]
)
with start_transaction(name="huggingface_hub tx"):
list(
client.text_generation(
prompt="hello",
stream=True,
)
)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.huggingface_hub"
sentry-python-2.18.0/tests/integrations/langchain/ 0000775 0000000 0000000 00000000000 14712146540 0022245 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/langchain/__init__.py 0000664 0000000 0000000 00000000065 14712146540 0024357 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("langchain_core")
sentry-python-2.18.0/tests/integrations/langchain/test_langchain.py 0000664 0000000 0000000 00000027167 14712146540 0025617 0 ustar 00root root 0000000 0000000 from typing import List, Optional, Any, Iterator
from unittest.mock import Mock
import pytest
try:
# Langchain >= 0.2
from langchain_openai import ChatOpenAI
except ImportError:
# Langchain < 0.2
from langchain_community.chat_models import ChatOpenAI
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.messages import BaseMessage, AIMessageChunk
from langchain_core.outputs import ChatGenerationChunk
from sentry_sdk import start_transaction
from sentry_sdk.integrations.langchain import LangchainIntegration
from langchain.agents import tool, AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
@tool
def get_word_length(word: str) -> int:
"""Returns the length of a word."""
return len(word)
global stream_result_mock # type: Mock
global llm_type # type: str
class MockOpenAI(ChatOpenAI):
def _stream(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> Iterator[ChatGenerationChunk]:
for x in stream_result_mock():
yield x
@property
def _llm_type(self) -> str:
return llm_type
def tiktoken_encoding_if_installed():
try:
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
return "cl100k_base"
except ImportError:
return None
@pytest.mark.parametrize(
"send_default_pii, include_prompts, use_unknown_llm_type",
[
(True, True, False),
(True, False, False),
(False, True, False),
(False, False, True),
],
)
def test_langchain_agent(
sentry_init, capture_events, send_default_pii, include_prompts, use_unknown_llm_type
):
global llm_type
llm_type = "acme-llm" if use_unknown_llm_type else "openai-chat"
sentry_init(
integrations=[
LangchainIntegration(
include_prompts=include_prompts,
tiktoken_encoding_name=tiktoken_encoding_if_installed(),
)
],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are very powerful assistant, but don't know current events",
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
global stream_result_mock
stream_result_mock = Mock(
side_effect=[
[
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(
content="",
additional_kwargs={
"tool_calls": [
{
"index": 0,
"id": "call_BbeyNhCKa6kYLYzrD40NGm3b",
"function": {
"arguments": "",
"name": "get_word_length",
},
"type": "function",
}
]
},
),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(
content="",
additional_kwargs={
"tool_calls": [
{
"index": 0,
"id": None,
"function": {
"arguments": '{"word": "eudca"}',
"name": None,
},
"type": None,
}
]
},
),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(content="5"),
generation_info={"finish_reason": "function_call"},
),
],
[
ChatGenerationChunk(
text="The word eudca has 5 letters.",
type="ChatGenerationChunk",
message=AIMessageChunk(content="The word eudca has 5 letters."),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
generation_info={"finish_reason": "stop"},
message=AIMessageChunk(content=""),
),
],
]
)
llm = MockOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
openai_api_key="badkey",
)
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
with start_transaction():
list(agent_executor.stream({"input": "How many letters in the word eudca"}))
tx = events[0]
assert tx["type"] == "transaction"
chat_spans = list(
x for x in tx["spans"] if x["op"] == "ai.chat_completions.create.langchain"
)
tool_exec_span = next(x for x in tx["spans"] if x["op"] == "ai.tool.langchain")
assert len(chat_spans) == 2
# We can't guarantee anything about the "shape" of the langchain execution graph
assert len(list(x for x in tx["spans"] if x["op"] == "ai.run.langchain")) > 0
if use_unknown_llm_type:
assert "ai_prompt_tokens_used" in chat_spans[0]["measurements"]
assert "ai_total_tokens_used" in chat_spans[0]["measurements"]
else:
# important: to avoid double counting, we do *not* measure
# tokens used if we have an explicit integration (e.g. OpenAI)
assert "measurements" not in chat_spans[0]
if send_default_pii and include_prompts:
assert (
"You are very powerful"
in chat_spans[0]["data"]["ai.input_messages"][0]["content"]
)
assert "5" in chat_spans[0]["data"]["ai.responses"]
assert "word" in tool_exec_span["data"]["ai.input_messages"]
assert 5 == int(tool_exec_span["data"]["ai.responses"])
assert (
"You are very powerful"
in chat_spans[1]["data"]["ai.input_messages"][0]["content"]
)
assert "5" in chat_spans[1]["data"]["ai.responses"]
else:
assert "ai.input_messages" not in chat_spans[0].get("data", {})
assert "ai.responses" not in chat_spans[0].get("data", {})
assert "ai.input_messages" not in chat_spans[1].get("data", {})
assert "ai.responses" not in chat_spans[1].get("data", {})
assert "ai.input_messages" not in tool_exec_span.get("data", {})
assert "ai.responses" not in tool_exec_span.get("data", {})
def test_langchain_error(sentry_init, capture_events):
sentry_init(
integrations=[LangchainIntegration(include_prompts=True)],
traces_sample_rate=1.0,
send_default_pii=True,
)
events = capture_events()
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are very powerful assistant, but don't know current events",
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
global stream_result_mock
stream_result_mock = Mock(side_effect=Exception("API rate limit error"))
llm = MockOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
openai_api_key="badkey",
)
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
with start_transaction(), pytest.raises(Exception):
list(agent_executor.stream({"input": "How many letters in the word eudca"}))
error = events[0]
assert error["level"] == "error"
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[LangchainIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are very powerful assistant, but don't know current events",
),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)
global stream_result_mock
stream_result_mock = Mock(
side_effect=[
[
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(
content="",
additional_kwargs={
"tool_calls": [
{
"index": 0,
"id": "call_BbeyNhCKa6kYLYzrD40NGm3b",
"function": {
"arguments": "",
"name": "get_word_length",
},
"type": "function",
}
]
},
),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(
content="",
additional_kwargs={
"tool_calls": [
{
"index": 0,
"id": None,
"function": {
"arguments": '{"word": "eudca"}',
"name": None,
},
"type": None,
}
]
},
),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
message=AIMessageChunk(content="5"),
generation_info={"finish_reason": "function_call"},
),
],
[
ChatGenerationChunk(
text="The word eudca has 5 letters.",
type="ChatGenerationChunk",
message=AIMessageChunk(content="The word eudca has 5 letters."),
),
ChatGenerationChunk(
type="ChatGenerationChunk",
generation_info={"finish_reason": "stop"},
message=AIMessageChunk(content=""),
),
],
]
)
llm = MockOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
openai_api_key="badkey",
)
agent = create_openai_tools_agent(llm, [get_word_length], prompt)
agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True)
with start_transaction():
list(agent_executor.stream({"input": "How many letters in the word eudca"}))
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.ai.langchain"
sentry-python-2.18.0/tests/integrations/launchdarkly/ 0000775 0000000 0000000 00000000000 14712146540 0023002 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/launchdarkly/__init__.py 0000664 0000000 0000000 00000000057 14712146540 0025115 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("ldclient")
sentry-python-2.18.0/tests/integrations/launchdarkly/test_launchdarkly.py 0000664 0000000 0000000 00000010113 14712146540 0027070 0 ustar 00root root 0000000 0000000 import asyncio
import concurrent.futures as cf
import ldclient
import sentry_sdk
import pytest
from ldclient import LDClient
from ldclient.config import Config
from ldclient.context import Context
from ldclient.integrations.test_data import TestData
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration
@pytest.mark.parametrize(
"use_global_client",
(False, True),
)
def test_launchdarkly_integration(sentry_init, use_global_client):
td = TestData.data_source()
config = Config("sdk-key", update_processor_class=td)
if use_global_client:
ldclient.set_config(config)
sentry_init(integrations=[LaunchDarklyIntegration()])
client = ldclient.get()
else:
client = LDClient(config=config)
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
# Set test values
td.update(td.flag("hello").variation_for_all(True))
td.update(td.flag("world").variation_for_all(True))
# Evaluate
client.variation("hello", Context.create("my-org", "organization"), False)
client.variation("world", Context.create("user1", "user"), False)
client.variation("other", Context.create("user2", "user"), False)
assert sentry_sdk.get_current_scope().flags.get() == [
{"flag": "hello", "result": True},
{"flag": "world", "result": True},
{"flag": "other", "result": False},
]
def test_launchdarkly_integration_threaded(sentry_init):
td = TestData.data_source()
client = LDClient(config=Config("sdk-key", update_processor_class=td))
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
context = Context.create("user1")
def task(flag_key):
# Creates a new isolation scope for the thread.
# This means the evaluations in each task are captured separately.
with sentry_sdk.isolation_scope():
client.variation(flag_key, context, False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
td.update(td.flag("hello").variation_for_all(True))
td.update(td.flag("world").variation_for_all(False))
# Capture an eval before we split isolation scopes.
client.variation("hello", context, False)
with cf.ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(task, ["world", "other"]))
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
def test_launchdarkly_integration_asyncio(sentry_init):
"""Assert concurrently evaluated flags do not pollute one another."""
td = TestData.data_source()
client = LDClient(config=Config("sdk-key", update_processor_class=td))
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
context = Context.create("user1")
async def task(flag_key):
with sentry_sdk.isolation_scope():
client.variation(flag_key, context, False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
async def runner():
return asyncio.gather(task("world"), task("other"))
td.update(td.flag("hello").variation_for_all(True))
td.update(td.flag("world").variation_for_all(False))
client.variation("hello", context, False)
results = asyncio.run(runner()).result()
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
def test_launchdarkly_integration_did_not_enable(monkeypatch):
# Client is not passed in and set_config wasn't called.
# TODO: Bad practice to access internals like this. We can skip this test, or remove this
# case entirely (force user to pass in a client instance).
ldclient._reset_client()
try:
ldclient.__lock.lock()
ldclient.__config = None
finally:
ldclient.__lock.unlock()
with pytest.raises(DidNotEnable):
LaunchDarklyIntegration()
# Client not initialized.
client = LDClient(config=Config("sdk-key"))
monkeypatch.setattr(client, "is_initialized", lambda: False)
with pytest.raises(DidNotEnable):
LaunchDarklyIntegration(ld_client=client)
sentry-python-2.18.0/tests/integrations/litestar/ 0000775 0000000 0000000 00000000000 14712146540 0022150 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/litestar/__init__.py 0000664 0000000 0000000 00000000057 14712146540 0024263 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("litestar")
sentry-python-2.18.0/tests/integrations/litestar/test_litestar.py 0000664 0000000 0000000 00000027511 14712146540 0025416 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import functools
import pytest
from sentry_sdk import capture_message
from sentry_sdk.integrations.litestar import LitestarIntegration
from typing import Any
from litestar import Litestar, get, Controller
from litestar.logging.config import LoggingConfig
from litestar.middleware import AbstractMiddleware
from litestar.middleware.logging import LoggingMiddlewareConfig
from litestar.middleware.rate_limit import RateLimitConfig
from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.testing import TestClient
def litestar_app_factory(middleware=None, debug=True, exception_handlers=None):
class MyController(Controller):
path = "/controller"
@get("/error")
async def controller_error(self) -> None:
raise Exception("Whoa")
@get("/some_url")
async def homepage_handler() -> "dict[str, Any]":
1 / 0
return {"status": "ok"}
@get("/custom_error", name="custom_name")
async def custom_error() -> Any:
raise Exception("Too Hot")
@get("/message")
async def message() -> "dict[str, Any]":
capture_message("hi")
return {"status": "ok"}
@get("/message/{message_id:str}")
async def message_with_id() -> "dict[str, Any]":
capture_message("hi")
return {"status": "ok"}
logging_config = LoggingConfig()
app = Litestar(
route_handlers=[
homepage_handler,
custom_error,
message,
message_with_id,
MyController,
],
debug=debug,
middleware=middleware,
logging_config=logging_config,
exception_handlers=exception_handlers,
)
return app
@pytest.mark.parametrize(
"test_url,expected_error,expected_message,expected_tx_name",
[
(
"/some_url",
ZeroDivisionError,
"division by zero",
"tests.integrations.litestar.test_litestar.litestar_app_factory..homepage_handler",
),
(
"/custom_error",
Exception,
"Too Hot",
"custom_name",
),
(
"/controller/error",
Exception,
"Whoa",
"tests.integrations.litestar.test_litestar.litestar_app_factory..MyController.controller_error",
),
],
)
def test_catch_exceptions(
sentry_init,
capture_exceptions,
capture_events,
test_url,
expected_error,
expected_message,
expected_tx_name,
):
sentry_init(integrations=[LitestarIntegration()])
litestar_app = litestar_app_factory()
exceptions = capture_exceptions()
events = capture_events()
client = TestClient(litestar_app)
try:
client.get(test_url)
except Exception:
pass
(exc,) = exceptions
assert isinstance(exc, expected_error)
assert str(exc) == expected_message
(event,) = events
assert expected_tx_name in event["transaction"]
assert event["exception"]["values"][0]["mechanism"]["type"] == "litestar"
def test_middleware_spans(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
integrations=[LitestarIntegration()],
)
logging_config = LoggingMiddlewareConfig()
session_config = ServerSideSessionConfig()
rate_limit_config = RateLimitConfig(rate_limit=("hour", 5))
litestar_app = litestar_app_factory(
middleware=[
session_config.middleware,
logging_config.middleware,
rate_limit_config.middleware,
]
)
events = capture_events()
client = TestClient(
litestar_app, raise_server_exceptions=False, base_url="http://testserver.local"
)
client.get("/message")
(_, transaction_event) = events
expected = {"SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"}
found = set()
litestar_spans = (
span
for span in transaction_event["spans"]
if span["op"] == "middleware.litestar"
)
for span in litestar_spans:
assert span["description"] in expected
assert span["description"] not in found
found.add(span["description"])
assert span["description"] == span["tags"]["litestar.middleware_name"]
def test_middleware_callback_spans(sentry_init, capture_events):
class SampleMiddleware(AbstractMiddleware):
async def __call__(self, scope, receive, send) -> None:
async def do_stuff(message):
if message["type"] == "http.response.start":
# do something here.
pass
await send(message)
await self.app(scope, receive, do_stuff)
sentry_init(
traces_sample_rate=1.0,
integrations=[LitestarIntegration()],
)
litestar_app = litestar_app_factory(middleware=[SampleMiddleware])
events = capture_events()
client = TestClient(litestar_app, raise_server_exceptions=False)
client.get("/message")
(_, transaction_events) = events
expected_litestar_spans = [
{
"op": "middleware.litestar",
"description": "SampleMiddleware",
"tags": {"litestar.middleware_name": "SampleMiddleware"},
},
{
"op": "middleware.litestar.send",
"description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send",
"tags": {"litestar.middleware_name": "SampleMiddleware"},
},
{
"op": "middleware.litestar.send",
"description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send",
"tags": {"litestar.middleware_name": "SampleMiddleware"},
},
]
def is_matching_span(expected_span, actual_span):
return (
expected_span["op"] == actual_span["op"]
and expected_span["description"] == actual_span["description"]
and expected_span["tags"] == actual_span["tags"]
)
actual_litestar_spans = list(
span
for span in transaction_events["spans"]
if "middleware.litestar" in span["op"]
)
assert len(actual_litestar_spans) == 3
for expected_span in expected_litestar_spans:
assert any(
is_matching_span(expected_span, actual_span)
for actual_span in actual_litestar_spans
)
def test_middleware_receive_send(sentry_init, capture_events):
class SampleReceiveSendMiddleware(AbstractMiddleware):
async def __call__(self, scope, receive, send):
message = await receive()
assert message
assert message["type"] == "http.request"
send_output = await send({"type": "something-unimportant"})
assert send_output is None
await self.app(scope, receive, send)
sentry_init(
traces_sample_rate=1.0,
integrations=[LitestarIntegration()],
)
litestar_app = litestar_app_factory(middleware=[SampleReceiveSendMiddleware])
client = TestClient(litestar_app, raise_server_exceptions=False)
# See SampleReceiveSendMiddleware.__call__ above for assertions of correct behavior
client.get("/message")
def test_middleware_partial_receive_send(sentry_init, capture_events):
class SamplePartialReceiveSendMiddleware(AbstractMiddleware):
async def __call__(self, scope, receive, send):
message = await receive()
assert message
assert message["type"] == "http.request"
send_output = await send({"type": "something-unimportant"})
assert send_output is None
async def my_receive(*args, **kwargs):
pass
async def my_send(*args, **kwargs):
pass
partial_receive = functools.partial(my_receive)
partial_send = functools.partial(my_send)
await self.app(scope, partial_receive, partial_send)
sentry_init(
traces_sample_rate=1.0,
integrations=[LitestarIntegration()],
)
litestar_app = litestar_app_factory(middleware=[SamplePartialReceiveSendMiddleware])
events = capture_events()
client = TestClient(litestar_app, raise_server_exceptions=False)
# See SamplePartialReceiveSendMiddleware.__call__ above for assertions of correct behavior
client.get("/message")
(_, transaction_events) = events
expected_litestar_spans = [
{
"op": "middleware.litestar",
"description": "SamplePartialReceiveSendMiddleware",
"tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"},
},
{
"op": "middleware.litestar.receive",
"description": "TestClientTransport.create_receive..receive",
"tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"},
},
{
"op": "middleware.litestar.send",
"description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send",
"tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"},
},
]
def is_matching_span(expected_span, actual_span):
return (
expected_span["op"] == actual_span["op"]
and actual_span["description"].startswith(expected_span["description"])
and expected_span["tags"] == actual_span["tags"]
)
actual_litestar_spans = list(
span
for span in transaction_events["spans"]
if "middleware.litestar" in span["op"]
)
assert len(actual_litestar_spans) == 3
for expected_span in expected_litestar_spans:
assert any(
is_matching_span(expected_span, actual_span)
for actual_span in actual_litestar_spans
)
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[LitestarIntegration()],
traces_sample_rate=1.0,
)
logging_config = LoggingMiddlewareConfig()
session_config = ServerSideSessionConfig()
rate_limit_config = RateLimitConfig(rate_limit=("hour", 5))
litestar_app = litestar_app_factory(
middleware=[
session_config.middleware,
logging_config.middleware,
rate_limit_config.middleware,
]
)
events = capture_events()
client = TestClient(
litestar_app, raise_server_exceptions=False, base_url="http://testserver.local"
)
client.get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.litestar"
for span in event["spans"]:
assert span["origin"] == "auto.http.litestar"
@pytest.mark.parametrize(
"is_send_default_pii",
[
True,
False,
],
ids=[
"send_default_pii=True",
"send_default_pii=False",
],
)
def test_litestar_scope_user_on_exception_event(
sentry_init, capture_exceptions, capture_events, is_send_default_pii
):
class TestUserMiddleware(AbstractMiddleware):
async def __call__(self, scope, receive, send):
scope["user"] = {
"email": "lennon@thebeatles.com",
"username": "john",
"id": "1",
}
await self.app(scope, receive, send)
sentry_init(
integrations=[LitestarIntegration()], send_default_pii=is_send_default_pii
)
litestar_app = litestar_app_factory(middleware=[TestUserMiddleware])
exceptions = capture_exceptions()
events = capture_events()
# This request intentionally raises an exception
client = TestClient(litestar_app)
try:
client.get("/some_url")
except Exception:
pass
assert len(exceptions) == 1
assert len(events) == 1
(event,) = events
if is_send_default_pii:
assert "user" in event
assert event["user"] == {
"email": "lennon@thebeatles.com",
"username": "john",
"id": "1",
}
else:
assert "user" not in event
sentry-python-2.18.0/tests/integrations/logging/ 0000775 0000000 0000000 00000000000 14712146540 0021747 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/logging/test_logging.py 0000664 0000000 0000000 00000015165 14712146540 0025016 0 ustar 00root root 0000000 0000000 import logging
import warnings
import pytest
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
other_logger = logging.getLogger("testfoo")
logger = logging.getLogger(__name__)
@pytest.fixture(autouse=True)
def reset_level():
other_logger.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
@pytest.mark.parametrize("logger", [logger, other_logger])
def test_logging_works_with_many_loggers(sentry_init, capture_events, logger):
sentry_init(integrations=[LoggingIntegration(event_level="ERROR")])
events = capture_events()
logger.info("bread")
logger.critical("LOL")
(event,) = events
assert event["level"] == "fatal"
assert not event["logentry"]["params"]
assert event["logentry"]["message"] == "LOL"
assert any(crumb["message"] == "bread" for crumb in event["breadcrumbs"]["values"])
@pytest.mark.parametrize("integrations", [None, [], [LoggingIntegration()]])
@pytest.mark.parametrize(
"kwargs", [{"exc_info": None}, {}, {"exc_info": 0}, {"exc_info": False}]
)
def test_logging_defaults(integrations, sentry_init, capture_events, kwargs):
sentry_init(integrations=integrations)
events = capture_events()
logger.info("bread")
logger.critical("LOL", **kwargs)
(event,) = events
assert event["level"] == "fatal"
assert any(crumb["message"] == "bread" for crumb in event["breadcrumbs"]["values"])
assert not any(
crumb["message"] == "LOL" for crumb in event["breadcrumbs"]["values"]
)
assert "threads" not in event
def test_logging_extra_data(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
logger.info("bread", extra=dict(foo=42))
logger.critical("lol", extra=dict(bar=69))
(event,) = events
assert event["level"] == "fatal"
assert event["extra"] == {"bar": 69}
assert any(
crumb["message"] == "bread" and crumb["data"] == {"foo": 42}
for crumb in event["breadcrumbs"]["values"]
)
def test_logging_extra_data_integer_keys(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
logger.critical("integer in extra keys", extra={1: 1})
(event,) = events
assert event["extra"] == {"1": 1}
def test_logging_stack(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
logger.error("first", exc_info=True)
logger.error("second")
(
event_with,
event_without,
) = events
assert event_with["level"] == "error"
assert event_with["threads"]["values"][0]["stacktrace"]["frames"]
assert event_without["level"] == "error"
assert "threads" not in event_without
def test_logging_level(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
logger.setLevel(logging.WARNING)
logger.error("hi")
(event,) = events
assert event["level"] == "error"
assert event["logentry"]["message"] == "hi"
del events[:]
logger.setLevel(logging.ERROR)
logger.warning("hi")
assert not events
def test_custom_log_level_names(sentry_init, capture_events):
levels = {
logging.DEBUG: "debug",
logging.INFO: "info",
logging.WARN: "warning",
logging.WARNING: "warning",
logging.ERROR: "error",
logging.CRITICAL: "fatal",
logging.FATAL: "fatal",
}
# set custom log level names
logging.addLevelName(logging.DEBUG, "custom level debüg: ")
logging.addLevelName(logging.INFO, "")
logging.addLevelName(logging.WARN, "custom level warn: ")
logging.addLevelName(logging.WARNING, "custom level warning: ")
logging.addLevelName(logging.ERROR, None)
logging.addLevelName(logging.CRITICAL, "custom level critical: ")
logging.addLevelName(logging.FATAL, "custom level 🔥: ")
for logging_level, sentry_level in levels.items():
logger.setLevel(logging_level)
sentry_init(
integrations=[LoggingIntegration(event_level=logging_level)],
default_integrations=False,
)
events = capture_events()
logger.log(logging_level, "Trying level %s", logging_level)
assert events
assert events[0]["level"] == sentry_level
assert events[0]["logentry"]["message"] == "Trying level %s"
assert events[0]["logentry"]["params"] == [logging_level]
del events[:]
def test_logging_filters(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
should_log = False
class MyFilter(logging.Filter):
def filter(self, record):
return should_log
logger.addFilter(MyFilter())
logger.error("hi")
assert not events
should_log = True
logger.error("hi")
(event,) = events
assert event["logentry"]["message"] == "hi"
def test_logging_captured_warnings(sentry_init, capture_events, recwarn):
sentry_init(
integrations=[LoggingIntegration(event_level="WARNING")],
default_integrations=False,
)
events = capture_events()
logging.captureWarnings(True)
warnings.warn("first", stacklevel=2)
warnings.warn("second", stacklevel=2)
logging.captureWarnings(False)
warnings.warn("third", stacklevel=2)
assert len(events) == 2
assert events[0]["level"] == "warning"
# Captured warnings start with the path where the warning was raised
assert "UserWarning: first" in events[0]["logentry"]["message"]
assert events[0]["logentry"]["params"] == []
assert events[1]["level"] == "warning"
assert "UserWarning: second" in events[1]["logentry"]["message"]
assert events[1]["logentry"]["params"] == []
# Using recwarn suppresses the "third" warning in the test output
assert len(recwarn) == 1
assert str(recwarn[0].message) == "third"
def test_ignore_logger(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
ignore_logger("testfoo")
other_logger.error("hi")
assert not events
def test_ignore_logger_wildcard(sentry_init, capture_events):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()
ignore_logger("testfoo.*")
nested_logger = logging.getLogger("testfoo.submodule")
logger.error("hi")
nested_logger.error("bye")
(event,) = events
assert event["logentry"]["message"] == "hi"
sentry-python-2.18.0/tests/integrations/loguru/ 0000775 0000000 0000000 00000000000 14712146540 0021636 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/loguru/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023747 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("loguru")
sentry-python-2.18.0/tests/integrations/loguru/test_loguru.py 0000664 0000000 0000000 00000006312 14712146540 0024566 0 ustar 00root root 0000000 0000000 import pytest
from loguru import logger
import sentry_sdk
from sentry_sdk.integrations.loguru import LoguruIntegration, LoggingLevels
logger.remove(0) # don't print to console
@pytest.mark.parametrize(
"level,created_event",
[
# None - no breadcrumb
# False - no event
# True - event created
(LoggingLevels.TRACE, None),
(LoggingLevels.DEBUG, None),
(LoggingLevels.INFO, False),
(LoggingLevels.SUCCESS, False),
(LoggingLevels.WARNING, False),
(LoggingLevels.ERROR, True),
(LoggingLevels.CRITICAL, True),
],
)
@pytest.mark.parametrize("disable_breadcrumbs", [True, False])
@pytest.mark.parametrize("disable_events", [True, False])
def test_just_log(
sentry_init,
capture_events,
level,
created_event,
disable_breadcrumbs,
disable_events,
):
sentry_init(
integrations=[
LoguruIntegration(
level=None if disable_breadcrumbs else LoggingLevels.INFO.value,
event_level=None if disable_events else LoggingLevels.ERROR.value,
)
],
default_integrations=False,
)
events = capture_events()
getattr(logger, level.name.lower())("test")
formatted_message = (
" | "
+ "{:9}".format(level.name.upper())
+ "| tests.integrations.loguru.test_loguru:test_just_log:46 - test"
)
if not created_event:
assert not events
breadcrumbs = sentry_sdk.get_isolation_scope()._breadcrumbs
if (
not disable_breadcrumbs and created_event is not None
): # not None == not TRACE or DEBUG level
(breadcrumb,) = breadcrumbs
assert breadcrumb["level"] == level.name.lower()
assert breadcrumb["category"] == "tests.integrations.loguru.test_loguru"
assert breadcrumb["message"][23:] == formatted_message
else:
assert not breadcrumbs
return
if disable_events:
assert not events
return
(event,) = events
assert event["level"] == (level.name.lower())
assert event["logger"] == "tests.integrations.loguru.test_loguru"
assert event["logentry"]["message"][23:] == formatted_message
def test_breadcrumb_format(sentry_init, capture_events):
sentry_init(
integrations=[
LoguruIntegration(
level=LoggingLevels.INFO.value,
event_level=None,
breadcrumb_format="{message}",
)
],
default_integrations=False,
)
logger.info("test")
formatted_message = "test"
breadcrumbs = sentry_sdk.get_isolation_scope()._breadcrumbs
(breadcrumb,) = breadcrumbs
assert breadcrumb["message"] == formatted_message
def test_event_format(sentry_init, capture_events):
sentry_init(
integrations=[
LoguruIntegration(
level=None,
event_level=LoggingLevels.ERROR.value,
event_format="{message}",
)
],
default_integrations=False,
)
events = capture_events()
logger.error("test")
formatted_message = "test"
(event,) = events
assert event["logentry"]["message"] == formatted_message
sentry-python-2.18.0/tests/integrations/modules/ 0000775 0000000 0000000 00000000000 14712146540 0021771 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/modules/test_modules.py 0000664 0000000 0000000 00000000557 14712146540 0025061 0 ustar 00root root 0000000 0000000 import sentry_sdk
from sentry_sdk.integrations.modules import ModulesIntegration
def test_basic(sentry_init, capture_events):
sentry_init(integrations=[ModulesIntegration()])
events = capture_events()
sentry_sdk.capture_exception(ValueError())
(event,) = events
assert "sentry-sdk" in event["modules"]
assert "pytest" in event["modules"]
sentry-python-2.18.0/tests/integrations/openai/ 0000775 0000000 0000000 00000000000 14712146540 0021574 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/openai/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023705 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("openai")
sentry-python-2.18.0/tests/integrations/openai/test_openai.py 0000664 0000000 0000000 00000065606 14712146540 0024475 0 ustar 00root root 0000000 0000000 import pytest
from openai import AsyncOpenAI, OpenAI, AsyncStream, Stream, OpenAIError
from openai.types import CompletionUsage, CreateEmbeddingResponse, Embedding
from openai.types.chat import ChatCompletion, ChatCompletionMessage, ChatCompletionChunk
from openai.types.chat.chat_completion import Choice
from openai.types.chat.chat_completion_chunk import ChoiceDelta, Choice as DeltaChoice
from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage
from sentry_sdk import start_transaction
from sentry_sdk.integrations.openai import (
OpenAIIntegration,
_calculate_chat_completion_usage,
)
from unittest import mock # python 3.3 and above
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)
EXAMPLE_CHAT_COMPLETION = ChatCompletion(
id="chat-id",
choices=[
Choice(
index=0,
finish_reason="stop",
message=ChatCompletionMessage(
role="assistant", content="the model response"
),
)
],
created=10000000,
model="model-id",
object="chat.completion",
usage=CompletionUsage(
completion_tokens=10,
prompt_tokens=20,
total_tokens=30,
),
)
async def async_iterator(values):
for value in values:
yield value
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_nonstreaming_chat_completion(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = OpenAI(api_key="z")
client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION)
with start_transaction(name="openai tx"):
response = (
client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
.choices[0]
.message.content
)
assert response == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]["content"]
assert "the model response" in span["data"]["ai.responses"]["content"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_nonstreaming_chat_completion_async(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
client.chat.completions._post = AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION)
with start_transaction(name="openai tx"):
response = await client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
response = response.choices[0].message.content
assert response == "the model response"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]["content"]
assert "the model response" in span["data"]["ai.responses"]["content"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 10
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
def tiktoken_encoding_if_installed():
try:
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
return "cl100k_base"
except ImportError:
return None
# noinspection PyTypeChecker
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_streaming_chat_completion(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[
OpenAIIntegration(
include_prompts=include_prompts,
tiktoken_encoding_name=tiktoken_encoding_if_installed(),
)
],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = OpenAI(api_key="z")
returned_stream = Stream(cast_to=None, response=None, client=client)
returned_stream._iterator = [
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=0, delta=ChoiceDelta(content="hel"), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=1, delta=ChoiceDelta(content="lo "), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=2, delta=ChoiceDelta(content="world"), finish_reason="stop"
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
]
client.chat.completions._post = mock.Mock(return_value=returned_stream)
with start_transaction(name="openai tx"):
response_stream = client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
response_string = "".join(
map(lambda x: x.choices[0].delta.content, response_stream)
)
assert response_string == "hello world"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]["content"]
assert "hello world" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
try:
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 2
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 1
assert span["measurements"]["ai_total_tokens_used"]["value"] == 3
except ImportError:
pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly
# noinspection PyTypeChecker
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_streaming_chat_completion_async(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[
OpenAIIntegration(
include_prompts=include_prompts,
tiktoken_encoding_name=tiktoken_encoding_if_installed(),
)
],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
returned_stream = AsyncStream(cast_to=None, response=None, client=client)
returned_stream._iterator = async_iterator(
[
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=0, delta=ChoiceDelta(content="hel"), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=1, delta=ChoiceDelta(content="lo "), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=2,
delta=ChoiceDelta(content="world"),
finish_reason="stop",
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
]
)
client.chat.completions._post = AsyncMock(return_value=returned_stream)
with start_transaction(name="openai tx"):
response_stream = await client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
response_string = ""
async for x in response_stream:
response_string += x.choices[0].delta.content
assert response_string == "hello world"
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.chat_completions.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]["content"]
assert "hello world" in span["data"]["ai.responses"]
else:
assert "ai.input_messages" not in span["data"]
assert "ai.responses" not in span["data"]
try:
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
assert span["measurements"]["ai_completion_tokens_used"]["value"] == 2
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 1
assert span["measurements"]["ai_total_tokens_used"]["value"] == 3
except ImportError:
pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly
def test_bad_chat_completion(sentry_init, capture_events):
sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0)
events = capture_events()
client = OpenAI(api_key="z")
client.chat.completions._post = mock.Mock(
side_effect=OpenAIError("API rate limit reached")
)
with pytest.raises(OpenAIError):
client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
(event,) = events
assert event["level"] == "error"
@pytest.mark.asyncio
async def test_bad_chat_completion_async(sentry_init, capture_events):
sentry_init(integrations=[OpenAIIntegration()], traces_sample_rate=1.0)
events = capture_events()
client = AsyncOpenAI(api_key="z")
client.chat.completions._post = AsyncMock(
side_effect=OpenAIError("API rate limit reached")
)
with pytest.raises(OpenAIError):
await client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
(event,) = events
assert event["level"] == "error"
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_embeddings_create(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = OpenAI(api_key="z")
returned_embedding = CreateEmbeddingResponse(
data=[Embedding(object="embedding", index=0, embedding=[1.0, 2.0, 3.0])],
model="some-model",
object="list",
usage=EmbeddingTokenUsage(
prompt_tokens=20,
total_tokens=30,
),
)
client.embeddings._post = mock.Mock(return_value=returned_embedding)
with start_transaction(name="openai tx"):
response = client.embeddings.create(
input="hello", model="text-embedding-3-large"
)
assert len(response.data[0].embedding) == 3
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.embeddings.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]
else:
assert "ai.input_messages" not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_embeddings_create_async(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
returned_embedding = CreateEmbeddingResponse(
data=[Embedding(object="embedding", index=0, embedding=[1.0, 2.0, 3.0])],
model="some-model",
object="list",
usage=EmbeddingTokenUsage(
prompt_tokens=20,
total_tokens=30,
),
)
client.embeddings._post = AsyncMock(return_value=returned_embedding)
with start_transaction(name="openai tx"):
response = await client.embeddings.create(
input="hello", model="text-embedding-3-large"
)
assert len(response.data[0].embedding) == 3
tx = events[0]
assert tx["type"] == "transaction"
span = tx["spans"][0]
assert span["op"] == "ai.embeddings.create.openai"
if send_default_pii and include_prompts:
assert "hello" in span["data"]["ai.input_messages"]
else:
assert "ai.input_messages" not in span["data"]
assert span["measurements"]["ai_prompt_tokens_used"]["value"] == 20
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_embeddings_create_raises_error(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = OpenAI(api_key="z")
client.embeddings._post = mock.Mock(
side_effect=OpenAIError("API rate limit reached")
)
with pytest.raises(OpenAIError):
client.embeddings.create(input="hello", model="text-embedding-3-large")
(event,) = events
assert event["level"] == "error"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"send_default_pii, include_prompts",
[(True, True), (True, False), (False, True), (False, False)],
)
async def test_embeddings_create_raises_error_async(
sentry_init, capture_events, send_default_pii, include_prompts
):
sentry_init(
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
client.embeddings._post = AsyncMock(
side_effect=OpenAIError("API rate limit reached")
)
with pytest.raises(OpenAIError):
await client.embeddings.create(input="hello", model="text-embedding-3-large")
(event,) = events
assert event["level"] == "error"
def test_span_origin_nonstreaming_chat(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = OpenAI(api_key="z")
client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION)
with start_transaction(name="openai tx"):
client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
@pytest.mark.asyncio
async def test_span_origin_nonstreaming_chat_async(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
client.chat.completions._post = AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION)
with start_transaction(name="openai tx"):
await client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
def test_span_origin_streaming_chat(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = OpenAI(api_key="z")
returned_stream = Stream(cast_to=None, response=None, client=client)
returned_stream._iterator = [
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=0, delta=ChoiceDelta(content="hel"), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=1, delta=ChoiceDelta(content="lo "), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=2, delta=ChoiceDelta(content="world"), finish_reason="stop"
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
]
client.chat.completions._post = mock.Mock(return_value=returned_stream)
with start_transaction(name="openai tx"):
response_stream = client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
"".join(map(lambda x: x.choices[0].delta.content, response_stream))
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
@pytest.mark.asyncio
async def test_span_origin_streaming_chat_async(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
returned_stream = AsyncStream(cast_to=None, response=None, client=client)
returned_stream._iterator = async_iterator(
[
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=0, delta=ChoiceDelta(content="hel"), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=1, delta=ChoiceDelta(content="lo "), finish_reason=None
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
ChatCompletionChunk(
id="1",
choices=[
DeltaChoice(
index=2,
delta=ChoiceDelta(content="world"),
finish_reason="stop",
)
],
created=100000,
model="model-id",
object="chat.completion.chunk",
),
]
)
client.chat.completions._post = AsyncMock(return_value=returned_stream)
with start_transaction(name="openai tx"):
response_stream = await client.chat.completions.create(
model="some-model", messages=[{"role": "system", "content": "hello"}]
)
async for _ in response_stream:
pass
# "".join(map(lambda x: x.choices[0].delta.content, response_stream))
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
def test_span_origin_embeddings(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = OpenAI(api_key="z")
returned_embedding = CreateEmbeddingResponse(
data=[Embedding(object="embedding", index=0, embedding=[1.0, 2.0, 3.0])],
model="some-model",
object="list",
usage=EmbeddingTokenUsage(
prompt_tokens=20,
total_tokens=30,
),
)
client.embeddings._post = mock.Mock(return_value=returned_embedding)
with start_transaction(name="openai tx"):
client.embeddings.create(input="hello", model="text-embedding-3-large")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
@pytest.mark.asyncio
async def test_span_origin_embeddings_async(sentry_init, capture_events):
sentry_init(
integrations=[OpenAIIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = AsyncOpenAI(api_key="z")
returned_embedding = CreateEmbeddingResponse(
data=[Embedding(object="embedding", index=0, embedding=[1.0, 2.0, 3.0])],
model="some-model",
object="list",
usage=EmbeddingTokenUsage(
prompt_tokens=20,
total_tokens=30,
),
)
client.embeddings._post = AsyncMock(return_value=returned_embedding)
with start_transaction(name="openai tx"):
await client.embeddings.create(input="hello", model="text-embedding-3-large")
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.ai.openai"
def test_calculate_chat_completion_usage_a():
span = mock.MagicMock()
def count_tokens(msg):
return len(str(msg))
response = mock.MagicMock()
response.usage = mock.MagicMock()
response.usage.completion_tokens = 10
response.usage.prompt_tokens = 20
response.usage.total_tokens = 30
messages = []
streaming_message_responses = []
with mock.patch(
"sentry_sdk.integrations.openai.record_token_usage"
) as mock_record_token_usage:
_calculate_chat_completion_usage(
messages, response, span, streaming_message_responses, count_tokens
)
mock_record_token_usage.assert_called_once_with(span, 20, 10, 30)
def test_calculate_chat_completion_usage_b():
span = mock.MagicMock()
def count_tokens(msg):
return len(str(msg))
response = mock.MagicMock()
response.usage = mock.MagicMock()
response.usage.completion_tokens = 10
response.usage.total_tokens = 10
messages = [
{"content": "one"},
{"content": "two"},
{"content": "three"},
]
streaming_message_responses = []
with mock.patch(
"sentry_sdk.integrations.openai.record_token_usage"
) as mock_record_token_usage:
_calculate_chat_completion_usage(
messages, response, span, streaming_message_responses, count_tokens
)
mock_record_token_usage.assert_called_once_with(span, 11, 10, 10)
def test_calculate_chat_completion_usage_c():
span = mock.MagicMock()
def count_tokens(msg):
return len(str(msg))
response = mock.MagicMock()
response.usage = mock.MagicMock()
response.usage.prompt_tokens = 20
response.usage.total_tokens = 20
messages = []
streaming_message_responses = [
"one",
"two",
"three",
]
with mock.patch(
"sentry_sdk.integrations.openai.record_token_usage"
) as mock_record_token_usage:
_calculate_chat_completion_usage(
messages, response, span, streaming_message_responses, count_tokens
)
mock_record_token_usage.assert_called_once_with(span, 20, 11, 20)
def test_calculate_chat_completion_usage_d():
span = mock.MagicMock()
def count_tokens(msg):
return len(str(msg))
response = mock.MagicMock()
response.usage = mock.MagicMock()
response.usage.prompt_tokens = 20
response.usage.total_tokens = 20
response.choices = [
mock.MagicMock(message="one"),
mock.MagicMock(message="two"),
mock.MagicMock(message="three"),
]
messages = []
streaming_message_responses = []
with mock.patch(
"sentry_sdk.integrations.openai.record_token_usage"
) as mock_record_token_usage:
_calculate_chat_completion_usage(
messages, response, span, streaming_message_responses, count_tokens
)
mock_record_token_usage.assert_called_once_with(span, 20, None, 20)
def test_calculate_chat_completion_usage_e():
span = mock.MagicMock()
def count_tokens(msg):
return len(str(msg))
response = mock.MagicMock()
messages = []
streaming_message_responses = None
with mock.patch(
"sentry_sdk.integrations.openai.record_token_usage"
) as mock_record_token_usage:
_calculate_chat_completion_usage(
messages, response, span, streaming_message_responses, count_tokens
)
mock_record_token_usage.assert_called_once_with(span, None, None, None)
sentry-python-2.18.0/tests/integrations/openfeature/ 0000775 0000000 0000000 00000000000 14712146540 0022636 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/openfeature/__init__.py 0000664 0000000 0000000 00000000062 14712146540 0024745 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("openfeature")
sentry-python-2.18.0/tests/integrations/openfeature/test_openfeature.py 0000664 0000000 0000000 00000005312 14712146540 0026565 0 ustar 00root root 0000000 0000000 import asyncio
import concurrent.futures as cf
import sentry_sdk
from openfeature import api
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
from sentry_sdk.integrations.openfeature import OpenFeatureIntegration
def test_openfeature_integration(sentry_init):
sentry_init(integrations=[OpenFeatureIntegration()])
flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()
client.get_boolean_value("hello", default_value=False)
client.get_boolean_value("world", default_value=False)
client.get_boolean_value("other", default_value=True)
assert sentry_sdk.get_current_scope().flags.get() == [
{"flag": "hello", "result": True},
{"flag": "world", "result": False},
{"flag": "other", "result": True},
]
def test_openfeature_integration_threaded(sentry_init):
sentry_init(integrations=[OpenFeatureIntegration()])
flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()
client.get_boolean_value("hello", default_value=False)
def task(flag):
# Create a new isolation scope for the thread. This means the flags
with sentry_sdk.isolation_scope():
client.get_boolean_value(flag, default_value=False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
with cf.ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(task, ["world", "other"]))
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
def test_openfeature_integration_asyncio(sentry_init):
"""Assert concurrently evaluated flags do not pollute one another."""
async def task(flag):
with sentry_sdk.isolation_scope():
client.get_boolean_value(flag, default_value=False)
return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()]
async def runner():
return asyncio.gather(task("world"), task("other"))
sentry_init(integrations=[OpenFeatureIntegration()])
flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()
client.get_boolean_value("hello", default_value=False)
results = asyncio.run(runner()).result()
assert results[0] == ["hello", "world"]
assert results[1] == ["hello", "other"]
sentry-python-2.18.0/tests/integrations/opentelemetry/ 0000775 0000000 0000000 00000000000 14712146540 0023215 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/opentelemetry/__init__.py 0000664 0000000 0000000 00000000064 14712146540 0025326 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("opentelemetry")
sentry-python-2.18.0/tests/integrations/opentelemetry/test_entry_points.py 0000664 0000000 0000000 00000001015 14712146540 0027360 0 ustar 00root root 0000000 0000000 import importlib
import os
from unittest.mock import patch
from opentelemetry import propagate
from sentry_sdk.integrations.opentelemetry import SentryPropagator
def test_propagator_loaded_if_mentioned_in_environment_variable():
try:
with patch.dict(os.environ, {"OTEL_PROPAGATORS": "sentry"}):
importlib.reload(propagate)
assert len(propagate.propagators) == 1
assert isinstance(propagate.propagators[0], SentryPropagator)
finally:
importlib.reload(propagate)
sentry-python-2.18.0/tests/integrations/opentelemetry/test_experimental.py 0000664 0000000 0000000 00000002467 14712146540 0027334 0 ustar 00root root 0000000 0000000 from unittest.mock import MagicMock, patch
import pytest
@pytest.mark.forked
def test_integration_enabled_if_option_is_on(sentry_init, reset_integrations):
mocked_setup_once = MagicMock()
with patch(
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration.setup_once",
mocked_setup_once,
):
sentry_init(
_experiments={
"otel_powered_performance": True,
},
)
mocked_setup_once.assert_called_once()
@pytest.mark.forked
def test_integration_not_enabled_if_option_is_off(sentry_init, reset_integrations):
mocked_setup_once = MagicMock()
with patch(
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration.setup_once",
mocked_setup_once,
):
sentry_init(
_experiments={
"otel_powered_performance": False,
},
)
mocked_setup_once.assert_not_called()
@pytest.mark.forked
def test_integration_not_enabled_if_option_is_missing(sentry_init, reset_integrations):
mocked_setup_once = MagicMock()
with patch(
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration.setup_once",
mocked_setup_once,
):
sentry_init()
mocked_setup_once.assert_not_called()
sentry-python-2.18.0/tests/integrations/opentelemetry/test_propagator.py 0000664 0000000 0000000 00000022120 14712146540 0027001 0 ustar 00root root 0000000 0000000 import pytest
from unittest import mock
from unittest.mock import MagicMock
from opentelemetry.context import get_current
from opentelemetry.trace import (
SpanContext,
TraceFlags,
set_span_in_context,
)
from opentelemetry.trace.propagation import get_current_span
from sentry_sdk.integrations.opentelemetry.consts import (
SENTRY_BAGGAGE_KEY,
SENTRY_TRACE_KEY,
)
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
from sentry_sdk.tracing_utils import Baggage
@pytest.mark.forked
def test_extract_no_context_no_sentry_trace_header():
"""
No context and NO Sentry trace data in getter.
Extract should return empty context.
"""
carrier = None
context = None
getter = MagicMock()
getter.get.return_value = None
modified_context = SentryPropagator().extract(carrier, context, getter)
assert modified_context == {}
@pytest.mark.forked
def test_extract_context_no_sentry_trace_header():
"""
Context but NO Sentry trace data in getter.
Extract should return context as is.
"""
carrier = None
context = {"some": "value"}
getter = MagicMock()
getter.get.return_value = None
modified_context = SentryPropagator().extract(carrier, context, getter)
assert modified_context == context
@pytest.mark.forked
def test_extract_empty_context_sentry_trace_header_no_baggage():
"""
Empty context but Sentry trace data but NO Baggage in getter.
Extract should return context that has empty baggage in it and also a NoopSpan with span_id and trace_id.
"""
carrier = None
context = {}
getter = MagicMock()
getter.get.side_effect = [
["1234567890abcdef1234567890abcdef-1234567890abcdef-1"],
None,
]
modified_context = SentryPropagator().extract(carrier, context, getter)
assert len(modified_context.keys()) == 3
assert modified_context[SENTRY_TRACE_KEY] == {
"trace_id": "1234567890abcdef1234567890abcdef",
"parent_span_id": "1234567890abcdef",
"parent_sampled": True,
}
assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == ""
span_context = get_current_span(modified_context).get_span_context()
assert span_context.span_id == int("1234567890abcdef", 16)
assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16)
@pytest.mark.forked
def test_extract_context_sentry_trace_header_baggage():
"""
Empty context but Sentry trace data and Baggage in getter.
Extract should return context that has baggage in it and also a NoopSpan with span_id and trace_id.
"""
baggage_header = (
"other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, "
"sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, "
"sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;"
)
carrier = None
context = {"some": "value"}
getter = MagicMock()
getter.get.side_effect = [
["1234567890abcdef1234567890abcdef-1234567890abcdef-1"],
[baggage_header],
]
modified_context = SentryPropagator().extract(carrier, context, getter)
assert len(modified_context.keys()) == 4
assert modified_context[SENTRY_TRACE_KEY] == {
"trace_id": "1234567890abcdef1234567890abcdef",
"parent_span_id": "1234567890abcdef",
"parent_sampled": True,
}
assert modified_context[SENTRY_BAGGAGE_KEY].serialize() == (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie"
)
span_context = get_current_span(modified_context).get_span_context()
assert span_context.span_id == int("1234567890abcdef", 16)
assert span_context.trace_id == int("1234567890abcdef1234567890abcdef", 16)
@pytest.mark.forked
def test_inject_empty_otel_span_map():
"""
Empty otel_span_map.
So there is no sentry_span to be found in inject()
and the function is returned early and no setters are called.
"""
carrier = None
context = get_current()
setter = MagicMock()
setter.set = MagicMock()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
trace_flags=TraceFlags(TraceFlags.SAMPLED),
is_remote=True,
)
span = MagicMock()
span.get_span_context.return_value = span_context
with mock.patch(
"sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span",
return_value=span,
):
full_context = set_span_in_context(span, context)
SentryPropagator().inject(carrier, full_context, setter)
setter.set.assert_not_called()
@pytest.mark.forked
def test_inject_sentry_span_no_baggage():
"""
Inject a sentry span with no baggage.
"""
carrier = None
context = get_current()
setter = MagicMock()
setter.set = MagicMock()
trace_id = "1234567890abcdef1234567890abcdef"
span_id = "1234567890abcdef"
span_context = SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
trace_flags=TraceFlags(TraceFlags.SAMPLED),
is_remote=True,
)
span = MagicMock()
span.get_span_context.return_value = span_context
sentry_span = MagicMock()
sentry_span.to_traceparent = mock.Mock(
return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1"
)
sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=None)
span_processor = SentrySpanProcessor()
span_processor.otel_span_map[span_id] = sentry_span
with mock.patch(
"sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span",
return_value=span,
):
full_context = set_span_in_context(span, context)
SentryPropagator().inject(carrier, full_context, setter)
setter.set.assert_called_once_with(
carrier,
"sentry-trace",
"1234567890abcdef1234567890abcdef-1234567890abcdef-1",
)
def test_inject_sentry_span_empty_baggage():
"""
Inject a sentry span with no baggage.
"""
carrier = None
context = get_current()
setter = MagicMock()
setter.set = MagicMock()
trace_id = "1234567890abcdef1234567890abcdef"
span_id = "1234567890abcdef"
span_context = SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
trace_flags=TraceFlags(TraceFlags.SAMPLED),
is_remote=True,
)
span = MagicMock()
span.get_span_context.return_value = span_context
sentry_span = MagicMock()
sentry_span.to_traceparent = mock.Mock(
return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1"
)
sentry_span.containing_transaction.get_baggage = mock.Mock(return_value=Baggage({}))
span_processor = SentrySpanProcessor()
span_processor.otel_span_map[span_id] = sentry_span
with mock.patch(
"sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span",
return_value=span,
):
full_context = set_span_in_context(span, context)
SentryPropagator().inject(carrier, full_context, setter)
setter.set.assert_called_once_with(
carrier,
"sentry-trace",
"1234567890abcdef1234567890abcdef-1234567890abcdef-1",
)
def test_inject_sentry_span_baggage():
"""
Inject a sentry span with baggage.
"""
carrier = None
context = get_current()
setter = MagicMock()
setter.set = MagicMock()
trace_id = "1234567890abcdef1234567890abcdef"
span_id = "1234567890abcdef"
span_context = SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
trace_flags=TraceFlags(TraceFlags.SAMPLED),
is_remote=True,
)
span = MagicMock()
span.get_span_context.return_value = span_context
sentry_span = MagicMock()
sentry_span.to_traceparent = mock.Mock(
return_value="1234567890abcdef1234567890abcdef-1234567890abcdef-1"
)
sentry_items = {
"sentry-trace_id": "771a43a4192642f0b136d5159a501700",
"sentry-public_key": "49d0f7386ad645858ae85020e393bef3",
"sentry-sample_rate": 0.01337,
"sentry-user_id": "Amélie",
}
baggage = Baggage(sentry_items=sentry_items)
sentry_span.containing_transaction.get_baggage = MagicMock(return_value=baggage)
span_processor = SentrySpanProcessor()
span_processor.otel_span_map[span_id] = sentry_span
with mock.patch(
"sentry_sdk.integrations.opentelemetry.propagator.trace.get_current_span",
return_value=span,
):
full_context = set_span_in_context(span, context)
SentryPropagator().inject(carrier, full_context, setter)
setter.set.assert_any_call(
carrier,
"sentry-trace",
"1234567890abcdef1234567890abcdef-1234567890abcdef-1",
)
setter.set.assert_any_call(
carrier,
"baggage",
baggage.serialize(),
)
sentry-python-2.18.0/tests/integrations/opentelemetry/test_span_processor.py 0000664 0000000 0000000 00000051365 14712146540 0027700 0 ustar 00root root 0000000 0000000 import time
from datetime import datetime, timezone
from unittest import mock
from unittest.mock import MagicMock
import pytest
from opentelemetry.trace import SpanKind, SpanContext, Status, StatusCode
import sentry_sdk
from sentry_sdk.integrations.opentelemetry.span_processor import (
SentrySpanProcessor,
link_trace_context_to_error_event,
)
from sentry_sdk.tracing import Span, Transaction
from sentry_sdk.tracing_utils import extract_sentrytrace_data
def test_is_sentry_span():
otel_span = MagicMock()
span_processor = SentrySpanProcessor()
assert not span_processor._is_sentry_span(otel_span)
client = MagicMock()
client.options = {"instrumenter": "otel"}
client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456"
sentry_sdk.get_global_scope().set_client(client)
assert not span_processor._is_sentry_span(otel_span)
otel_span.attributes = {
"http.url": "https://example.com",
}
assert not span_processor._is_sentry_span(otel_span)
otel_span.attributes = {
"http.url": "https://o123456.ingest.sentry.io/api/123/envelope",
}
assert span_processor._is_sentry_span(otel_span)
def test_get_otel_context():
otel_span = MagicMock()
otel_span.attributes = {"foo": "bar"}
otel_span.resource = MagicMock()
otel_span.resource.attributes = {"baz": "qux"}
span_processor = SentrySpanProcessor()
otel_context = span_processor._get_otel_context(otel_span)
assert otel_context == {
"attributes": {"foo": "bar"},
"resource": {"baz": "qux"},
}
def test_get_trace_data_with_span_and_trace():
otel_span = MagicMock()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = None
parent_context = {}
span_processor = SentrySpanProcessor()
sentry_trace_data = span_processor._get_trace_data(otel_span, parent_context)
assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef"
assert sentry_trace_data["span_id"] == "1234567890abcdef"
assert sentry_trace_data["parent_span_id"] is None
assert sentry_trace_data["parent_sampled"] is None
assert sentry_trace_data["baggage"] is None
def test_get_trace_data_with_span_and_trace_and_parent():
otel_span = MagicMock()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
span_processor = SentrySpanProcessor()
sentry_trace_data = span_processor._get_trace_data(otel_span, parent_context)
assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef"
assert sentry_trace_data["span_id"] == "1234567890abcdef"
assert sentry_trace_data["parent_span_id"] == "abcdef1234567890"
assert sentry_trace_data["parent_sampled"] is None
assert sentry_trace_data["baggage"] is None
def test_get_trace_data_with_sentry_trace():
otel_span = MagicMock()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.get_value",
side_effect=[
extract_sentrytrace_data(
"1234567890abcdef1234567890abcdef-1234567890abcdef-1"
),
None,
],
):
span_processor = SentrySpanProcessor()
sentry_trace_data = span_processor._get_trace_data(otel_span, parent_context)
assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef"
assert sentry_trace_data["span_id"] == "1234567890abcdef"
assert sentry_trace_data["parent_span_id"] == "abcdef1234567890"
assert sentry_trace_data["parent_sampled"] is True
assert sentry_trace_data["baggage"] is None
with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.get_value",
side_effect=[
extract_sentrytrace_data(
"1234567890abcdef1234567890abcdef-1234567890abcdef-0"
),
None,
],
):
span_processor = SentrySpanProcessor()
sentry_trace_data = span_processor._get_trace_data(otel_span, parent_context)
assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef"
assert sentry_trace_data["span_id"] == "1234567890abcdef"
assert sentry_trace_data["parent_span_id"] == "abcdef1234567890"
assert sentry_trace_data["parent_sampled"] is False
assert sentry_trace_data["baggage"] is None
def test_get_trace_data_with_sentry_trace_and_baggage():
otel_span = MagicMock()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
baggage = (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=0.01337,sentry-user_id=Am%C3%A9lie"
)
with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.get_value",
side_effect=[
extract_sentrytrace_data(
"1234567890abcdef1234567890abcdef-1234567890abcdef-1"
),
baggage,
],
):
span_processor = SentrySpanProcessor()
sentry_trace_data = span_processor._get_trace_data(otel_span, parent_context)
assert sentry_trace_data["trace_id"] == "1234567890abcdef1234567890abcdef"
assert sentry_trace_data["span_id"] == "1234567890abcdef"
assert sentry_trace_data["parent_span_id"] == "abcdef1234567890"
assert sentry_trace_data["parent_sampled"]
assert sentry_trace_data["baggage"] == baggage
def test_update_span_with_otel_data_http_method():
sentry_span = Span()
otel_span = MagicMock()
otel_span.name = "Test OTel Span"
otel_span.kind = SpanKind.CLIENT
otel_span.attributes = {
"http.method": "GET",
"http.status_code": 429,
"http.status_text": "xxx",
"http.user_agent": "curl/7.64.1",
"net.peer.name": "example.com",
"http.target": "/",
}
span_processor = SentrySpanProcessor()
span_processor._update_span_with_otel_data(sentry_span, otel_span)
assert sentry_span.op == "http.client"
assert sentry_span.description == "GET example.com /"
assert sentry_span.status == "resource_exhausted"
assert sentry_span._data["http.method"] == "GET"
assert sentry_span._data["http.response.status_code"] == 429
assert sentry_span._data["http.status_text"] == "xxx"
assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
assert sentry_span._data["net.peer.name"] == "example.com"
assert sentry_span._data["http.target"] == "/"
@pytest.mark.parametrize(
"otel_status, expected_status",
[
pytest.param(Status(StatusCode.UNSET), None, id="unset"),
pytest.param(Status(StatusCode.OK), "ok", id="ok"),
pytest.param(Status(StatusCode.ERROR), "internal_error", id="error"),
],
)
def test_update_span_with_otel_status(otel_status, expected_status):
sentry_span = Span()
otel_span = MagicMock()
otel_span.name = "Test OTel Span"
otel_span.kind = SpanKind.INTERNAL
otel_span.status = otel_status
span_processor = SentrySpanProcessor()
span_processor._update_span_with_otel_status(sentry_span, otel_span)
assert sentry_span.get_trace_context().get("status") == expected_status
def test_update_span_with_otel_data_http_method2():
sentry_span = Span()
otel_span = MagicMock()
otel_span.name = "Test OTel Span"
otel_span.kind = SpanKind.SERVER
otel_span.attributes = {
"http.method": "GET",
"http.status_code": 429,
"http.status_text": "xxx",
"http.user_agent": "curl/7.64.1",
"http.url": "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef",
}
span_processor = SentrySpanProcessor()
span_processor._update_span_with_otel_data(sentry_span, otel_span)
assert sentry_span.op == "http.server"
assert sentry_span.description == "GET https://example.com/status/403"
assert sentry_span.status == "resource_exhausted"
assert sentry_span._data["http.method"] == "GET"
assert sentry_span._data["http.response.status_code"] == 429
assert sentry_span._data["http.status_text"] == "xxx"
assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
assert (
sentry_span._data["http.url"]
== "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef"
)
def test_update_span_with_otel_data_db_query():
sentry_span = Span()
otel_span = MagicMock()
otel_span.name = "Test OTel Span"
otel_span.attributes = {
"db.system": "postgresql",
"db.statement": "SELECT * FROM table where pwd = '123456'",
}
span_processor = SentrySpanProcessor()
span_processor._update_span_with_otel_data(sentry_span, otel_span)
assert sentry_span.op == "db"
assert sentry_span.description == "SELECT * FROM table where pwd = '123456'"
assert sentry_span._data["db.system"] == "postgresql"
assert (
sentry_span._data["db.statement"] == "SELECT * FROM table where pwd = '123456'"
)
def test_on_start_transaction():
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.start_time = time.time_ns()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
fake_start_transaction = MagicMock()
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456"
sentry_sdk.get_global_scope().set_client(fake_client)
with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.start_transaction",
fake_start_transaction,
):
span_processor = SentrySpanProcessor()
span_processor.on_start(otel_span, parent_context)
fake_start_transaction.assert_called_once_with(
name="Sample OTel Span",
span_id="1234567890abcdef",
parent_span_id="abcdef1234567890",
trace_id="1234567890abcdef1234567890abcdef",
baggage=None,
start_timestamp=datetime.fromtimestamp(
otel_span.start_time / 1e9, timezone.utc
),
instrumenter="otel",
origin="auto.otel",
)
assert len(span_processor.otel_span_map.keys()) == 1
assert list(span_processor.otel_span_map.keys())[0] == "1234567890abcdef"
def test_on_start_child():
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.start_time = time.time_ns()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456"
sentry_sdk.get_global_scope().set_client(fake_client)
fake_span = MagicMock()
span_processor = SentrySpanProcessor()
span_processor.otel_span_map["abcdef1234567890"] = fake_span
span_processor.on_start(otel_span, parent_context)
fake_span.start_child.assert_called_once_with(
span_id="1234567890abcdef",
name="Sample OTel Span",
start_timestamp=datetime.fromtimestamp(
otel_span.start_time / 1e9, timezone.utc
),
instrumenter="otel",
origin="auto.otel",
)
assert len(span_processor.otel_span_map.keys()) == 2
assert "abcdef1234567890" in span_processor.otel_span_map.keys()
assert "1234567890abcdef" in span_processor.otel_span_map.keys()
def test_on_end_no_sentry_span():
"""
If on_end is called on a span that is not in the otel_span_map, it should be a no-op.
"""
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.end_time = time.time_ns()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
span_processor = SentrySpanProcessor()
span_processor.otel_span_map = {}
span_processor._get_otel_context = MagicMock()
span_processor._update_span_with_otel_data = MagicMock()
span_processor.on_end(otel_span)
span_processor._get_otel_context.assert_not_called()
span_processor._update_span_with_otel_data.assert_not_called()
def test_on_end_sentry_transaction():
"""
Test on_end for a sentry Transaction.
"""
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.end_time = time.time_ns()
otel_span.status = Status(StatusCode.OK)
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
sentry_sdk.get_global_scope().set_client(fake_client)
fake_sentry_span = MagicMock(spec=Transaction)
fake_sentry_span.set_context = MagicMock()
fake_sentry_span.finish = MagicMock()
span_processor = SentrySpanProcessor()
span_processor._get_otel_context = MagicMock()
span_processor._update_span_with_otel_data = MagicMock()
span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span
span_processor.on_end(otel_span)
fake_sentry_span.set_context.assert_called_once()
span_processor._update_span_with_otel_data.assert_not_called()
fake_sentry_span.set_status.assert_called_once_with("ok")
fake_sentry_span.finish.assert_called_once()
def test_on_end_sentry_span():
"""
Test on_end for a sentry Span.
"""
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.end_time = time.time_ns()
otel_span.status = Status(StatusCode.OK)
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
sentry_sdk.get_global_scope().set_client(fake_client)
fake_sentry_span = MagicMock(spec=Span)
fake_sentry_span.set_context = MagicMock()
fake_sentry_span.finish = MagicMock()
span_processor = SentrySpanProcessor()
span_processor._get_otel_context = MagicMock()
span_processor._update_span_with_otel_data = MagicMock()
span_processor.otel_span_map["1234567890abcdef"] = fake_sentry_span
span_processor.on_end(otel_span)
fake_sentry_span.set_context.assert_not_called()
span_processor._update_span_with_otel_data.assert_called_once_with(
fake_sentry_span, otel_span
)
fake_sentry_span.set_status.assert_called_once_with("ok")
fake_sentry_span.finish.assert_called_once()
def test_link_trace_context_to_error_event():
"""
Test that the trace context is added to the error event.
"""
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
sentry_sdk.get_global_scope().set_client(fake_client)
span_id = "1234567890abcdef"
trace_id = "1234567890abcdef1234567890abcdef"
fake_trace_context = {
"bla": "blub",
"foo": "bar",
"baz": 123,
}
sentry_span = MagicMock()
sentry_span.get_trace_context = MagicMock(return_value=fake_trace_context)
otel_span_map = {
span_id: sentry_span,
}
span_context = SpanContext(
trace_id=int(trace_id, 16),
span_id=int(span_id, 16),
is_remote=True,
)
otel_span = MagicMock()
otel_span.get_span_context = MagicMock(return_value=span_context)
fake_event = {"event_id": "1234567890abcdef1234567890abcdef"}
with mock.patch(
"sentry_sdk.integrations.opentelemetry.span_processor.get_current_span",
return_value=otel_span,
):
event = link_trace_context_to_error_event(fake_event, otel_span_map)
assert event
assert event == fake_event # the event is changed in place inside the function
assert "contexts" in event
assert "trace" in event["contexts"]
assert event["contexts"]["trace"] == fake_trace_context
def test_pruning_old_spans_on_start():
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.start_time = time.time_ns()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
parent_context = {}
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel", "debug": False}
fake_client.dsn = "https://1234567890abcdef@o123456.ingest.sentry.io/123456"
sentry_sdk.get_global_scope().set_client(fake_client)
span_processor = SentrySpanProcessor()
span_processor.otel_span_map = {
"111111111abcdef": MagicMock(), # should stay
"2222222222abcdef": MagicMock(), # should go
"3333333333abcdef": MagicMock(), # should go
}
current_time_minutes = int(time.time() / 60)
span_processor.open_spans = {
current_time_minutes - 3: {"111111111abcdef"}, # should stay
current_time_minutes
- 11: {"2222222222abcdef", "3333333333abcdef"}, # should go
}
span_processor.on_start(otel_span, parent_context)
assert sorted(list(span_processor.otel_span_map.keys())) == [
"111111111abcdef",
"1234567890abcdef",
]
assert sorted(list(span_processor.open_spans.values())) == [
{"111111111abcdef"},
{"1234567890abcdef"},
]
def test_pruning_old_spans_on_end():
otel_span = MagicMock()
otel_span.name = "Sample OTel Span"
otel_span.start_time = time.time_ns()
span_context = SpanContext(
trace_id=int("1234567890abcdef1234567890abcdef", 16),
span_id=int("1234567890abcdef", 16),
is_remote=True,
)
otel_span.get_span_context.return_value = span_context
otel_span.parent = MagicMock()
otel_span.parent.span_id = int("abcdef1234567890", 16)
fake_client = MagicMock()
fake_client.options = {"instrumenter": "otel"}
sentry_sdk.get_global_scope().set_client(fake_client)
fake_sentry_span = MagicMock(spec=Span)
fake_sentry_span.set_context = MagicMock()
fake_sentry_span.finish = MagicMock()
span_processor = SentrySpanProcessor()
span_processor._get_otel_context = MagicMock()
span_processor._update_span_with_otel_data = MagicMock()
span_processor.otel_span_map = {
"111111111abcdef": MagicMock(), # should stay
"2222222222abcdef": MagicMock(), # should go
"3333333333abcdef": MagicMock(), # should go
"1234567890abcdef": fake_sentry_span, # should go (because it is closed)
}
current_time_minutes = int(time.time() / 60)
span_processor.open_spans = {
current_time_minutes: {"1234567890abcdef"}, # should go (because it is closed)
current_time_minutes - 3: {"111111111abcdef"}, # should stay
current_time_minutes
- 11: {"2222222222abcdef", "3333333333abcdef"}, # should go
}
span_processor.on_end(otel_span)
assert sorted(list(span_processor.otel_span_map.keys())) == ["111111111abcdef"]
assert sorted(list(span_processor.open_spans.values())) == [{"111111111abcdef"}]
sentry-python-2.18.0/tests/integrations/pure_eval/ 0000775 0000000 0000000 00000000000 14712146540 0022303 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/pure_eval/__init__.py 0000664 0000000 0000000 00000000060 14712146540 0024410 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("pure_eval")
sentry-python-2.18.0/tests/integrations/pure_eval/test_pure_eval.py 0000664 0000000 0000000 00000004412 14712146540 0025677 0 ustar 00root root 0000000 0000000 from types import SimpleNamespace
import pytest
from sentry_sdk import capture_exception, serializer
from sentry_sdk.integrations.pure_eval import PureEvalIntegration
@pytest.mark.parametrize("integrations", [[], [PureEvalIntegration()]])
def test_include_local_variables_enabled(sentry_init, capture_events, integrations):
sentry_init(include_local_variables=True, integrations=integrations)
events = capture_events()
def foo():
namespace = SimpleNamespace()
q = 1
w = 2
e = 3
r = 4
t = 5
y = 6
u = 7
i = 8
o = 9
p = 10
a = 11
s = 12
str((q, w, e, r, t, y, u, i, o, p, a, s)) # use variables for linter
namespace.d = {1: 2}
print(namespace.d[1] / 0)
# Appearances of variables after the main statement don't affect order
print(q)
print(s)
print(events)
try:
foo()
except Exception:
capture_exception()
(event,) = events
assert all(
frame["vars"]
for frame in event["exception"]["values"][0]["stacktrace"]["frames"]
)
frame_vars = event["exception"]["values"][0]["stacktrace"]["frames"][-1]["vars"]
if integrations:
# Values closest to the exception line appear first
# Test this order if possible given the Python version and dict order
expected_keys = [
"namespace",
"namespace.d",
"namespace.d[1]",
"s",
"a",
"p",
"o",
"i",
"u",
"y",
]
assert list(frame_vars.keys()) == expected_keys
assert frame_vars["namespace.d"] == {"1": "2"}
assert frame_vars["namespace.d[1]"] == "2"
else:
# Without pure_eval, the variables are unpredictable.
# In later versions, those at the top appear first and are thus included
assert frame_vars.keys() <= {
"namespace",
"q",
"w",
"e",
"r",
"t",
"y",
"u",
"i",
"o",
"p",
"a",
"s",
"events",
}
assert len(frame_vars) == serializer.MAX_DATABAG_BREADTH
sentry-python-2.18.0/tests/integrations/pymongo/ 0000775 0000000 0000000 00000000000 14712146540 0022011 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/pymongo/__init__.py 0000664 0000000 0000000 00000000056 14712146540 0024123 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("pymongo")
sentry-python-2.18.0/tests/integrations/pymongo/test_pymongo.py 0000664 0000000 0000000 00000036523 14712146540 0025123 0 ustar 00root root 0000000 0000000 from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.pymongo import PyMongoIntegration, _strip_pii
from mockupdb import MockupDB, OpQuery
from pymongo import MongoClient
import pytest
@pytest.fixture(scope="session")
def mongo_server():
server = MockupDB(verbose=True)
server.autoresponds("ismaster", maxWireVersion=6)
server.run()
server.autoresponds(
{"find": "test_collection"}, cursor={"id": 123, "firstBatch": []}
)
# Find query changed somewhere between PyMongo 3.1 and 3.12.
# This line is to respond to "find" queries sent by old PyMongo the same way it's done above.
server.autoresponds(OpQuery({"foobar": 1}), cursor={"id": 123, "firstBatch": []})
server.autoresponds({"insert": "test_collection"}, ok=1)
server.autoresponds({"insert": "erroneous"}, ok=0, errmsg="test error")
yield server
server.stop()
@pytest.mark.parametrize("with_pii", [False, True])
def test_transactions(sentry_init, capture_events, mongo_server, with_pii):
sentry_init(
integrations=[PyMongoIntegration()],
traces_sample_rate=1.0,
send_default_pii=with_pii,
)
events = capture_events()
connection = MongoClient(mongo_server.uri)
with start_transaction():
list(
connection["test_db"]["test_collection"].find({"foobar": 1})
) # force query execution
connection["test_db"]["test_collection"].insert_one({"foo": 2})
try:
connection["test_db"]["erroneous"].insert_many([{"bar": 3}, {"baz": 4}])
pytest.fail("Request should raise")
except Exception:
pass
(event,) = events
(find, insert_success, insert_fail) = event["spans"]
common_tags = {
"db.name": "test_db",
"db.system": "mongodb",
"net.peer.name": mongo_server.host,
"net.peer.port": str(mongo_server.port),
}
for span in find, insert_success, insert_fail:
assert span["data"][SPANDATA.DB_SYSTEM] == "mongodb"
assert span["data"][SPANDATA.DB_NAME] == "test_db"
assert span["data"][SPANDATA.SERVER_ADDRESS] == "localhost"
assert span["data"][SPANDATA.SERVER_PORT] == mongo_server.port
for field, value in common_tags.items():
assert span["tags"][field] == value
assert span["data"][field] == value
assert find["op"] == "db"
assert insert_success["op"] == "db"
assert insert_fail["op"] == "db"
assert find["data"]["db.operation"] == "find"
assert find["tags"]["db.operation"] == "find"
assert insert_success["data"]["db.operation"] == "insert"
assert insert_success["tags"]["db.operation"] == "insert"
assert insert_fail["data"]["db.operation"] == "insert"
assert insert_fail["tags"]["db.operation"] == "insert"
assert find["description"].startswith('{"find')
assert insert_success["description"].startswith('{"insert')
assert insert_fail["description"].startswith('{"insert')
assert find["data"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection"
assert find["tags"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection"
assert insert_success["data"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection"
assert insert_success["tags"][SPANDATA.DB_MONGODB_COLLECTION] == "test_collection"
assert insert_fail["data"][SPANDATA.DB_MONGODB_COLLECTION] == "erroneous"
assert insert_fail["tags"][SPANDATA.DB_MONGODB_COLLECTION] == "erroneous"
if with_pii:
assert "1" in find["description"]
assert "2" in insert_success["description"]
assert "3" in insert_fail["description"] and "4" in insert_fail["description"]
else:
# All values in filter replaced by "%s"
assert "1" not in find["description"]
# All keys below top level replaced by "%s"
assert "2" not in insert_success["description"]
assert (
"3" not in insert_fail["description"]
and "4" not in insert_fail["description"]
)
assert find["tags"]["status"] == "ok"
assert insert_success["tags"]["status"] == "ok"
assert insert_fail["tags"]["status"] == "internal_error"
@pytest.mark.parametrize("with_pii", [False, True])
def test_breadcrumbs(sentry_init, capture_events, mongo_server, with_pii):
sentry_init(
integrations=[PyMongoIntegration()],
traces_sample_rate=1.0,
send_default_pii=with_pii,
)
events = capture_events()
connection = MongoClient(mongo_server.uri)
list(
connection["test_db"]["test_collection"].find({"foobar": 1})
) # force query execution
capture_message("hi")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["category"] == "query"
assert crumb["message"].startswith('{"find')
if with_pii:
assert "1" in crumb["message"]
else:
assert "1" not in crumb["message"]
assert crumb["type"] == "db"
assert crumb["data"] == {
"db.name": "test_db",
"db.system": "mongodb",
"db.operation": "find",
"net.peer.name": mongo_server.host,
"net.peer.port": str(mongo_server.port),
"db.mongodb.collection": "test_collection",
}
@pytest.mark.parametrize(
"testcase",
[
{
"command": {
"insert": "my_collection",
"ordered": True,
"documents": [
{
"username": "anton2",
"email": "anton@somewhere.io",
"password": "c4e86722fb56d946f7ddeecdae47e1c4458bf98a0a3ee5d5113111adf7bf0175",
"_id": "635bc7403cb4f8a736f61cf2",
}
],
},
"command_stripped": {
"insert": "my_collection",
"ordered": True,
"documents": [
{"username": "%s", "email": "%s", "password": "%s", "_id": "%s"}
],
},
},
{
"command": {
"insert": "my_collection",
"ordered": True,
"documents": [
{
"username": "indiana4",
"email": "indy@jones.org",
"password": "63e86722fb56d946f7ddeecdae47e1c4458bf98a0a3ee5d5113111adf7bf016b",
"_id": "635bc7403cb4f8a736f61cf3",
}
],
},
"command_stripped": {
"insert": "my_collection",
"ordered": True,
"documents": [
{"username": "%s", "email": "%s", "password": "%s", "_id": "%s"}
],
},
},
{
"command": {
"find": "my_collection",
"filter": {},
"limit": 1,
"singleBatch": True,
},
"command_stripped": {
"find": "my_collection",
"filter": {},
"limit": 1,
"singleBatch": True,
},
},
{
"command": {
"find": "my_collection",
"filter": {"username": "notthere"},
"limit": 1,
"singleBatch": True,
},
"command_stripped": {
"find": "my_collection",
"filter": {"username": "%s"},
"limit": 1,
"singleBatch": True,
},
},
{
"command": {
"insert": "my_collection",
"ordered": True,
"documents": [
{
"username": "userx1",
"email": "x@somewhere.io",
"password": "ccc86722fb56d946f7ddeecdae47e1c4458bf98a0a3ee5d5113111adf7bf0175",
"_id": "635bc7403cb4f8a736f61cf4",
},
{
"username": "userx2",
"email": "x@somewhere.io",
"password": "xxx86722fb56d946f7ddeecdae47e1c4458bf98a0a3ee5d5113111adf7bf0175",
"_id": "635bc7403cb4f8a736f61cf5",
},
],
},
"command_stripped": {
"insert": "my_collection",
"ordered": True,
"documents": [
{"username": "%s", "email": "%s", "password": "%s", "_id": "%s"},
{"username": "%s", "email": "%s", "password": "%s", "_id": "%s"},
],
},
},
{
"command": {
"find": "my_collection",
"filter": {"email": "ada@lovelace.com"},
},
"command_stripped": {"find": "my_collection", "filter": {"email": "%s"}},
},
{
"command": {
"aggregate": "my_collection",
"pipeline": [{"$match": {}}, {"$group": {"_id": 1, "n": {"$sum": 1}}}],
"cursor": {},
},
"command_stripped": {
"aggregate": "my_collection",
"pipeline": [{"$match": {}}, {"$group": {"_id": 1, "n": {"$sum": 1}}}],
"cursor": "%s",
},
},
{
"command": {
"aggregate": "my_collection",
"pipeline": [
{"$match": {"email": "x@somewhere.io"}},
{"$group": {"_id": 1, "n": {"$sum": 1}}},
],
"cursor": {},
},
"command_stripped": {
"aggregate": "my_collection",
"pipeline": [
{"$match": {"email": "%s"}},
{"$group": {"_id": 1, "n": {"$sum": 1}}},
],
"cursor": "%s",
},
},
{
"command": {
"createIndexes": "my_collection",
"indexes": [{"name": "username_1", "key": [("username", 1)]}],
},
"command_stripped": {
"createIndexes": "my_collection",
"indexes": [{"name": "username_1", "key": [("username", 1)]}],
},
},
{
"command": {
"update": "my_collection",
"ordered": True,
"updates": [
("q", {"email": "anton@somewhere.io"}),
(
"u",
{
"email": "anton2@somwehre.io",
"extra_field": "extra_content",
"new": "bla",
},
),
("multi", False),
("upsert", False),
],
},
"command_stripped": {
"update": "my_collection",
"ordered": True,
"updates": "%s",
},
},
{
"command": {
"update": "my_collection",
"ordered": True,
"updates": [
("q", {"email": "anton2@somwehre.io"}),
("u", {"$rename": {"new": "new_field"}}),
("multi", False),
("upsert", False),
],
},
"command_stripped": {
"update": "my_collection",
"ordered": True,
"updates": "%s",
},
},
{
"command": {
"update": "my_collection",
"ordered": True,
"updates": [
("q", {"email": "x@somewhere.io"}),
("u", {"$rename": {"password": "pwd"}}),
("multi", True),
("upsert", False),
],
},
"command_stripped": {
"update": "my_collection",
"ordered": True,
"updates": "%s",
},
},
{
"command": {
"delete": "my_collection",
"ordered": True,
"deletes": [("q", {"username": "userx2"}), ("limit", 1)],
},
"command_stripped": {
"delete": "my_collection",
"ordered": True,
"deletes": "%s",
},
},
{
"command": {
"delete": "my_collection",
"ordered": True,
"deletes": [("q", {"email": "xplus@somewhere.io"}), ("limit", 0)],
},
"command_stripped": {
"delete": "my_collection",
"ordered": True,
"deletes": "%s",
},
},
{
"command": {
"findAndModify": "my_collection",
"query": {"email": "ada@lovelace.com"},
"new": False,
"remove": True,
},
"command_stripped": {
"findAndModify": "my_collection",
"query": {"email": "%s"},
"new": "%s",
"remove": "%s",
},
},
{
"command": {
"findAndModify": "my_collection",
"query": {"email": "anton2@somewhere.io"},
"new": False,
"update": {"email": "anton3@somwehre.io", "extra_field": "xxx"},
"upsert": False,
},
"command_stripped": {
"findAndModify": "my_collection",
"query": {"email": "%s"},
"new": "%s",
"update": {"email": "%s", "extra_field": "%s"},
"upsert": "%s",
},
},
{
"command": {
"findAndModify": "my_collection",
"query": {"email": "anton3@somewhere.io"},
"new": False,
"update": {"$rename": {"extra_field": "extra_field2"}},
"upsert": False,
},
"command_stripped": {
"findAndModify": "my_collection",
"query": {"email": "%s"},
"new": "%s",
"update": {"$rename": "%s"},
"upsert": "%s",
},
},
{
"command": {
"renameCollection": "test.my_collection",
"to": "test.new_collection",
},
"command_stripped": {
"renameCollection": "test.my_collection",
"to": "test.new_collection",
},
},
{
"command": {"drop": "new_collection"},
"command_stripped": {"drop": "new_collection"},
},
],
)
def test_strip_pii(testcase):
assert _strip_pii(testcase["command"]) == testcase["command_stripped"]
def test_span_origin(sentry_init, capture_events, mongo_server):
sentry_init(
integrations=[PyMongoIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
connection = MongoClient(mongo_server.uri)
with start_transaction():
list(
connection["test_db"]["test_collection"].find({"foobar": 1})
) # force query execution
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.pymongo"
sentry-python-2.18.0/tests/integrations/pyramid/ 0000775 0000000 0000000 00000000000 14712146540 0021766 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/pyramid/__init__.py 0000664 0000000 0000000 00000000056 14712146540 0024100 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("pyramid")
sentry-python-2.18.0/tests/integrations/pyramid/test_pyramid.py 0000664 0000000 0000000 00000027666 14712146540 0025065 0 ustar 00root root 0000000 0000000 import json
import logging
from io import BytesIO
import pyramid.testing
import pytest
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.response import Response
from werkzeug.test import Client
from sentry_sdk import capture_message, add_breadcrumb
from sentry_sdk.integrations.pyramid import PyramidIntegration
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
from tests.conftest import unpack_werkzeug_response
try:
from importlib.metadata import version
PYRAMID_VERSION = tuple(map(int, version("pyramid").split(".")))
except ImportError:
# < py3.8
import pkg_resources
PYRAMID_VERSION = tuple(
map(int, pkg_resources.get_distribution("pyramid").version.split("."))
)
def hi(request):
capture_message("hi")
return Response("hi")
def hi_with_id(request):
capture_message("hi with id")
return Response("hi with id")
@pytest.fixture
def pyramid_config():
config = pyramid.testing.setUp()
try:
config.add_route("hi", "/message")
config.add_view(hi, route_name="hi")
config.add_route("hi_with_id", "/message/{message_id}")
config.add_view(hi_with_id, route_name="hi_with_id")
yield config
finally:
pyramid.testing.tearDown()
@pytest.fixture
def route(pyramid_config):
def inner(url):
def wrapper(f):
pyramid_config.add_route(f.__name__, url)
pyramid_config.add_view(f, route_name=f.__name__)
return f
return wrapper
return inner
@pytest.fixture
def get_client(pyramid_config):
def inner():
return Client(pyramid_config.make_wsgi_app())
return inner
def test_view_exceptions(
get_client, route, sentry_init, capture_events, capture_exceptions
):
sentry_init(integrations=[PyramidIntegration()])
events = capture_events()
exceptions = capture_exceptions()
add_breadcrumb({"message": "hi"})
@route("/errors")
def errors(request):
add_breadcrumb({"message": "hi2"})
1 / 0
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/errors")
(error,) = exceptions
assert isinstance(error, ZeroDivisionError)
(event,) = events
(breadcrumb,) = event["breadcrumbs"]["values"]
assert breadcrumb["message"] == "hi2"
# Checking only the last value in the exceptions list,
# because Pyramid >= 1.9 returns a chained exception and before just a single exception
assert event["exception"]["values"][-1]["mechanism"]["type"] == "pyramid"
assert event["exception"]["values"][-1]["type"] == "ZeroDivisionError"
def test_has_context(route, get_client, sentry_init, capture_events):
sentry_init(integrations=[PyramidIntegration()])
events = capture_events()
@route("/context_message/{msg}")
def hi2(request):
capture_message(request.matchdict["msg"])
return Response("hi")
client = get_client()
client.get("/context_message/yoo")
(event,) = events
assert event["message"] == "yoo"
assert event["request"] == {
"env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
"headers": {"Host": "localhost"},
"method": "GET",
"query_string": "",
"url": "http://localhost/context_message/yoo",
}
assert event["transaction"] == "hi2"
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
("/message", "route_name", "hi", "component"),
("/message", "route_pattern", "/message", "route"),
("/message/123456", "route_name", "hi_with_id", "component"),
("/message/123456", "route_pattern", "/message/{message_id}", "route"),
],
)
def test_transaction_style(
sentry_init,
get_client,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(integrations=[PyramidIntegration(transaction_style=transaction_style)])
events = capture_events()
client = get_client()
client.get(url)
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
def test_large_json_request(sentry_init, capture_events, route, get_client):
sentry_init(integrations=[PyramidIntegration()])
data = {"foo": {"bar": "a" * 2000}}
@route("/")
def index(request):
assert request.json == data
assert request.text == json.dumps(data)
assert not request.POST
capture_message("hi")
return Response("ok")
events = capture_events()
client = get_client()
client.post("/", content_type="application/json", data=json.dumps(data))
(event,) = events
assert event["_meta"]["request"]["data"]["foo"]["bar"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]["bar"]) == 1024
@pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"])
def test_flask_empty_json_request(sentry_init, capture_events, route, get_client, data):
sentry_init(integrations=[PyramidIntegration()])
@route("/")
def index(request):
assert request.json == data
assert request.text == json.dumps(data)
assert not request.POST
capture_message("hi")
return Response("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
def test_json_not_truncated_if_max_request_body_size_is_always(
sentry_init, capture_events, route, get_client
):
sentry_init(integrations=[PyramidIntegration()], max_request_body_size="always")
data = {
"key{}".format(i): "value{}".format(i) for i in range(MAX_DATABAG_BREADTH + 10)
}
@route("/")
def index(request):
assert request.json == data
assert request.text == json.dumps(data)
capture_message("hi")
return Response("ok")
events = capture_events()
client = get_client()
client.post("/", content_type="application/json", data=json.dumps(data))
(event,) = events
assert event["request"]["data"] == data
def test_files_and_form(sentry_init, capture_events, route, get_client):
sentry_init(integrations=[PyramidIntegration()], max_request_body_size="always")
data = {"foo": "a" * 2000, "file": (BytesIO(b"hello"), "hello.txt")}
@route("/")
def index(request):
capture_message("hi")
return Response("ok")
events = capture_events()
client = get_client()
client.post("/", data=data)
(event,) = events
assert event["_meta"]["request"]["data"]["foo"] == {
"": {"len": 2000, "rem": [["!limit", "x", 1021, 1024]]}
}
assert len(event["request"]["data"]["foo"]) == 1024
assert event["_meta"]["request"]["data"]["file"] == {"": {"rem": [["!raw", "x"]]}}
assert not event["request"]["data"]["file"]
def test_bad_request_not_captured(
sentry_init, pyramid_config, capture_events, route, get_client
):
import pyramid.httpexceptions as exc
sentry_init(integrations=[PyramidIntegration()])
events = capture_events()
@route("/")
def index(request):
raise exc.HTTPBadRequest()
def errorhandler(exc, request):
return Response("bad request")
pyramid_config.add_view(errorhandler, context=exc.HTTPBadRequest)
client = get_client()
client.get("/")
assert not events
def test_errorhandler_ok(
sentry_init, pyramid_config, capture_exceptions, route, get_client
):
sentry_init(integrations=[PyramidIntegration()])
errors = capture_exceptions()
@route("/")
def index(request):
raise Exception()
def errorhandler(exc, request):
return Response("bad request")
pyramid_config.add_view(errorhandler, context=Exception)
client = get_client()
client.get("/")
assert not errors
@pytest.mark.skipif(
PYRAMID_VERSION < (1, 9),
reason="We don't have the right hooks in older Pyramid versions",
)
def test_errorhandler_500(
sentry_init, pyramid_config, capture_exceptions, route, get_client
):
sentry_init(integrations=[PyramidIntegration()])
errors = capture_exceptions()
@route("/")
def index(request):
1 / 0
def errorhandler(exc, request):
return Response("bad request", status=500)
pyramid_config.add_view(errorhandler, context=Exception)
client = get_client()
app_iter, status, headers = unpack_werkzeug_response(client.get("/"))
assert app_iter == b"bad request"
assert status.lower() == "500 internal server error"
(error,) = errors
assert isinstance(error, ZeroDivisionError)
def test_error_in_errorhandler(
sentry_init, pyramid_config, capture_events, route, get_client
):
sentry_init(integrations=[PyramidIntegration()])
@route("/")
def index(request):
raise ValueError()
def error_handler(err, request):
1 / 0
pyramid_config.add_view(error_handler, context=ValueError)
events = capture_events()
client = get_client()
with pytest.raises(ZeroDivisionError):
client.get("/")
(event,) = events
exception = event["exception"]["values"][-1]
assert exception["type"] == "ZeroDivisionError"
def test_error_in_authenticated_userid(
sentry_init, pyramid_config, capture_events, route, get_client
):
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_init(
send_default_pii=True,
integrations=[
PyramidIntegration(),
LoggingIntegration(event_level=logging.ERROR),
],
)
logger = logging.getLogger("test_pyramid")
class AuthenticationPolicy:
def authenticated_userid(self, request):
logger.warning("failed to identify user")
pyramid_config.set_authorization_policy(ACLAuthorizationPolicy())
pyramid_config.set_authentication_policy(AuthenticationPolicy())
events = capture_events()
client = get_client()
client.get("/message")
assert len(events) == 1
# In `authenticated_userid` there used to be a call to `logging.error`. This would print this error in the
# event processor of the Pyramid integration and the logging integration would capture this and send it to Sentry.
# This is not possible anymore, because capturing that error in the logging integration would again run all the
# event processors (from the global, isolation and current scope) and thus would again run the same pyramid
# event processor that raised the error in the first place, leading on an infinite loop.
# This test here is now deactivated and always passes, but it is kept here to document the problem.
# This change in behavior is also mentioned in the migration documentation for Python SDK 2.0
# assert "message" not in events[0].keys()
def tween_factory(handler, registry):
def tween(request):
try:
response = handler(request)
except Exception:
mroute = request.matched_route
if mroute and mroute.name in ("index",):
return Response("bad request", status_code=400)
return response
return tween
def test_tween_ok(sentry_init, pyramid_config, capture_exceptions, route, get_client):
sentry_init(integrations=[PyramidIntegration()])
errors = capture_exceptions()
@route("/")
def index(request):
raise Exception()
pyramid_config.add_tween(
"tests.integrations.pyramid.test_pyramid.tween_factory",
under=pyramid.tweens.INGRESS,
)
client = get_client()
client.get("/")
assert not errors
def test_span_origin(sentry_init, capture_events, get_client):
sentry_init(
integrations=[PyramidIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
client = get_client()
client.get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.pyramid"
sentry-python-2.18.0/tests/integrations/quart/ 0000775 0000000 0000000 00000000000 14712146540 0021455 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/quart/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0023565 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("quart")
sentry-python-2.18.0/tests/integrations/quart/test_quart.py 0000664 0000000 0000000 00000036723 14712146540 0024235 0 ustar 00root root 0000000 0000000 import json
import threading
from unittest import mock
import pytest
import sentry_sdk
from sentry_sdk import (
set_tag,
capture_message,
capture_exception,
)
from sentry_sdk.integrations.logging import LoggingIntegration
import sentry_sdk.integrations.quart as quart_sentry
from quart import Quart, Response, abort, stream_with_context
from quart.views import View
from quart_auth import AuthUser, login_user
try:
from quart_auth import QuartAuth
auth_manager = QuartAuth()
except ImportError:
from quart_auth import AuthManager
auth_manager = AuthManager()
def quart_app_factory():
app = Quart(__name__)
app.debug = False
app.config["TESTING"] = False
app.secret_key = "haha"
auth_manager.init_app(app)
@app.route("/message")
async def hi():
capture_message("hi")
return "ok"
@app.route("/message/")
async def hi_with_id(message_id):
capture_message("hi with id")
return "ok with id"
@app.get("/sync/thread_ids")
def _thread_ids_sync():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}
@app.get("/async/thread_ids")
async def _thread_ids_async():
return {
"main": str(threading.main_thread().ident),
"active": str(threading.current_thread().ident),
}
return app
@pytest.fixture(params=("manual",))
def integration_enabled_params(request):
if request.param == "manual":
return {"integrations": [quart_sentry.QuartIntegration()]}
else:
raise ValueError(request.param)
@pytest.mark.asyncio
async def test_has_context(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
client = app.test_client()
response = await client.get("/message")
assert response.status_code == 200
(event,) = events
assert event["transaction"] == "hi"
assert "data" not in event["request"]
assert event["request"]["url"] == "http://localhost/message"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"url,transaction_style,expected_transaction,expected_source",
[
("/message", "endpoint", "hi", "component"),
("/message", "url", "/message", "route"),
("/message/123456", "endpoint", "hi_with_id", "component"),
("/message/123456", "url", "/message/", "route"),
],
)
async def test_transaction_style(
sentry_init,
capture_events,
url,
transaction_style,
expected_transaction,
expected_source,
):
sentry_init(
integrations=[
quart_sentry.QuartIntegration(transaction_style=transaction_style)
]
)
app = quart_app_factory()
events = capture_events()
client = app.test_client()
response = await client.get(url)
assert response.status_code == 200
(event,) = events
assert event["transaction"] == expected_transaction
@pytest.mark.asyncio
async def test_errors(
sentry_init,
capture_exceptions,
capture_events,
integration_enabled_params,
):
sentry_init(**integration_enabled_params)
app = quart_app_factory()
@app.route("/")
async def index():
1 / 0
exceptions = capture_exceptions()
events = capture_events()
client = app.test_client()
try:
await client.get("/")
except ZeroDivisionError:
pass
(exc,) = exceptions
assert isinstance(exc, ZeroDivisionError)
(event,) = events
assert event["exception"]["values"][0]["mechanism"]["type"] == "quart"
@pytest.mark.asyncio
async def test_quart_auth_not_installed(
sentry_init, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
app = quart_app_factory()
monkeypatch.setattr(quart_sentry, "quart_auth", None)
events = capture_events()
client = app.test_client()
await client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
@pytest.mark.asyncio
async def test_quart_auth_not_configured(
sentry_init, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
app = quart_app_factory()
assert quart_sentry.quart_auth
events = capture_events()
client = app.test_client()
await client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
@pytest.mark.asyncio
async def test_quart_auth_partially_configured(
sentry_init, capture_events, monkeypatch, integration_enabled_params
):
sentry_init(**integration_enabled_params)
app = quart_app_factory()
events = capture_events()
client = app.test_client()
await client.get("/message")
(event,) = events
assert event.get("user", {}).get("id") is None
@pytest.mark.asyncio
@pytest.mark.parametrize("send_default_pii", [True, False])
@pytest.mark.parametrize("user_id", [None, "42", "3"])
async def test_quart_auth_configured(
send_default_pii,
sentry_init,
user_id,
capture_events,
monkeypatch,
integration_enabled_params,
):
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
app = quart_app_factory()
@app.route("/login")
async def login():
if user_id is not None:
login_user(AuthUser(user_id))
return "ok"
events = capture_events()
client = app.test_client()
assert (await client.get("/login")).status_code == 200
assert not events
assert (await client.get("/message")).status_code == 200
(event,) = events
if user_id is None or not send_default_pii:
assert event.get("user", {}).get("id") is None
else:
assert event["user"]["id"] == str(user_id)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"integrations",
[
[quart_sentry.QuartIntegration()],
[quart_sentry.QuartIntegration(), LoggingIntegration(event_level="ERROR")],
],
)
async def test_errors_not_reported_twice(sentry_init, integrations, capture_events):
sentry_init(integrations=integrations)
app = quart_app_factory()
@app.route("/")
async def index():
try:
1 / 0
except Exception as e:
app.logger.exception(e)
raise e
events = capture_events()
client = app.test_client()
# with pytest.raises(ZeroDivisionError):
await client.get("/")
assert len(events) == 1
@pytest.mark.asyncio
async def test_logging(sentry_init, capture_events):
# ensure that Quart's logger magic doesn't break ours
sentry_init(
integrations=[
quart_sentry.QuartIntegration(),
LoggingIntegration(event_level="ERROR"),
]
)
app = quart_app_factory()
@app.route("/")
async def index():
app.logger.error("hi")
return "ok"
events = capture_events()
client = app.test_client()
await client.get("/")
(event,) = events
assert event["level"] == "error"
@pytest.mark.asyncio
async def test_no_errors_without_request(sentry_init):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
async with app.app_context():
capture_exception(ValueError())
def test_cli_commands_raise():
app = quart_app_factory()
if not hasattr(app, "cli"):
pytest.skip("Too old quart version")
from quart.cli import ScriptInfo
@app.cli.command()
def foo():
1 / 0
with pytest.raises(ZeroDivisionError):
app.cli.main(
args=["foo"], prog_name="myapp", obj=ScriptInfo(create_app=lambda _: app)
)
@pytest.mark.asyncio
async def test_500(sentry_init):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
@app.route("/")
async def index():
1 / 0
@app.errorhandler(500)
async def error_handler(err):
return "Sentry error."
client = app.test_client()
response = await client.get("/")
assert (await response.get_data(as_text=True)) == "Sentry error."
@pytest.mark.asyncio
async def test_error_in_errorhandler(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
@app.route("/")
async def index():
raise ValueError()
@app.errorhandler(500)
async def error_handler(err):
1 / 0
events = capture_events()
client = app.test_client()
with pytest.raises(ZeroDivisionError):
await client.get("/")
event1, event2 = events
(exception,) = event1["exception"]["values"]
assert exception["type"] == "ValueError"
exception = event2["exception"]["values"][-1]
assert exception["type"] == "ZeroDivisionError"
@pytest.mark.asyncio
async def test_bad_request_not_captured(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@app.route("/")
async def index():
abort(400)
client = app.test_client()
await client.get("/")
assert not events
@pytest.mark.asyncio
async def test_does_not_leak_scope(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
sentry_sdk.get_isolation_scope().set_tag("request_data", False)
@app.route("/")
async def index():
sentry_sdk.get_isolation_scope().set_tag("request_data", True)
async def generate():
for row in range(1000):
assert sentry_sdk.get_isolation_scope()._tags["request_data"]
yield str(row) + "\n"
return Response(stream_with_context(generate)(), mimetype="text/csv")
client = app.test_client()
response = await client.get("/")
assert (await response.get_data(as_text=True)) == "".join(
str(row) + "\n" for row in range(1000)
)
assert not events
assert not sentry_sdk.get_isolation_scope()._tags["request_data"]
@pytest.mark.asyncio
async def test_scoped_test_client(sentry_init):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
@app.route("/")
async def index():
return "ok"
async with app.test_client() as client:
response = await client.get("/")
assert response.status_code == 200
@pytest.mark.asyncio
@pytest.mark.parametrize("exc_cls", [ZeroDivisionError, Exception])
async def test_errorhandler_for_exception_swallows_exception(
sentry_init, capture_events, exc_cls
):
# In contrast to error handlers for a status code, error
# handlers for exceptions can swallow the exception (this is
# just how the Quart signal works)
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@app.route("/")
async def index():
1 / 0
@app.errorhandler(exc_cls)
async def zerodivision(e):
return "ok"
async with app.test_client() as client:
response = await client.get("/")
assert response.status_code == 200
assert not events
@pytest.mark.asyncio
async def test_tracing_success(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
@app.before_request
async def _():
set_tag("before_request", "yes")
@app.route("/message_tx")
async def hi_tx():
set_tag("view", "yes")
capture_message("hi")
return "ok"
events = capture_events()
async with app.test_client() as client:
response = await client.get("/message_tx")
assert response.status_code == 200
message_event, transaction_event = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "hi_tx"
assert transaction_event["tags"]["view"] == "yes"
assert transaction_event["tags"]["before_request"] == "yes"
assert message_event["message"] == "hi"
assert message_event["transaction"] == "hi_tx"
assert message_event["tags"]["view"] == "yes"
assert message_event["tags"]["before_request"] == "yes"
@pytest.mark.asyncio
async def test_tracing_error(sentry_init, capture_events):
sentry_init(traces_sample_rate=1.0, integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@app.route("/error")
async def error():
1 / 0
async with app.test_client() as client:
response = await client.get("/error")
assert response.status_code == 500
error_event, transaction_event = events
assert transaction_event["type"] == "transaction"
assert transaction_event["transaction"] == "error"
assert error_event["transaction"] == "error"
(exception,) = error_event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
@pytest.mark.asyncio
async def test_class_based_views(sentry_init, capture_events):
sentry_init(integrations=[quart_sentry.QuartIntegration()])
app = quart_app_factory()
events = capture_events()
@app.route("/")
class HelloClass(View):
methods = ["GET"]
async def dispatch_request(self):
capture_message("hi")
return "ok"
app.add_url_rule("/hello-class/", view_func=HelloClass.as_view("hello_class"))
async with app.test_client() as client:
response = await client.get("/hello-class/")
assert response.status_code == 200
(event,) = events
assert event["message"] == "hi"
assert event["transaction"] == "hello_class"
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
@pytest.mark.asyncio
async def test_active_thread_id(
sentry_init, capture_envelopes, teardown_profiling, endpoint
):
with mock.patch(
"sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0
):
sentry_init(
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
app = quart_app_factory()
envelopes = capture_envelopes()
async with app.test_client() as client:
response = await client.get(endpoint)
assert response.status_code == 200
data = json.loads(await response.get_data(as_text=True))
envelopes = [envelope for envelope in envelopes]
assert len(envelopes) == 1
profiles = [item for item in envelopes[0].items if item.type == "profile"]
assert len(profiles) == 1, envelopes[0].items
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
async def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[quart_sentry.QuartIntegration()],
traces_sample_rate=1.0,
)
app = quart_app_factory()
events = capture_events()
client = app.test_client()
await client.get("/message")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.quart"
sentry-python-2.18.0/tests/integrations/ray/ 0000775 0000000 0000000 00000000000 14712146540 0021114 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/ray/__init__.py 0000664 0000000 0000000 00000000052 14712146540 0023222 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("ray")
sentry-python-2.18.0/tests/integrations/ray/test_ray.py 0000664 0000000 0000000 00000013447 14712146540 0023331 0 ustar 00root root 0000000 0000000 import json
import os
import pytest
import ray
import sentry_sdk
from sentry_sdk.envelope import Envelope
from sentry_sdk.integrations.ray import RayIntegration
from tests.conftest import TestTransport
class RayTestTransport(TestTransport):
def __init__(self):
self.envelopes = []
super().__init__()
def capture_envelope(self, envelope: Envelope) -> None:
self.envelopes.append(envelope)
class RayLoggingTransport(TestTransport):
def __init__(self):
super().__init__()
def capture_envelope(self, envelope: Envelope) -> None:
print(envelope.serialize().decode("utf-8", "replace"))
def setup_sentry_with_logging_transport():
setup_sentry(transport=RayLoggingTransport())
def setup_sentry(transport=None):
sentry_sdk.init(
integrations=[RayIntegration()],
transport=RayTestTransport() if transport is None else transport,
traces_sample_rate=1.0,
)
@pytest.mark.forked
def test_ray_tracing():
setup_sentry()
ray.init(
runtime_env={
"worker_process_setup_hook": setup_sentry,
"working_dir": "./",
}
)
@ray.remote
def example_task():
with sentry_sdk.start_span(op="task", name="example task step"):
...
return sentry_sdk.get_client().transport.envelopes
with sentry_sdk.start_transaction(op="task", name="ray test transaction"):
worker_envelopes = ray.get(example_task.remote())
client_envelope = sentry_sdk.get_client().transport.envelopes[0]
client_transaction = client_envelope.get_transaction_event()
worker_envelope = worker_envelopes[0]
worker_transaction = worker_envelope.get_transaction_event()
assert (
client_transaction["contexts"]["trace"]["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
)
for span in client_transaction["spans"]:
assert (
span["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
)
for span in worker_transaction["spans"]:
assert (
span["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
)
@pytest.mark.forked
def test_ray_spans():
setup_sentry()
ray.init(
runtime_env={
"worker_process_setup_hook": setup_sentry,
"working_dir": "./",
}
)
@ray.remote
def example_task():
return sentry_sdk.get_client().transport.envelopes
with sentry_sdk.start_transaction(op="task", name="ray test transaction"):
worker_envelopes = ray.get(example_task.remote())
client_envelope = sentry_sdk.get_client().transport.envelopes[0]
client_transaction = client_envelope.get_transaction_event()
worker_envelope = worker_envelopes[0]
worker_transaction = worker_envelope.get_transaction_event()
for span in client_transaction["spans"]:
assert span["op"] == "queue.submit.ray"
assert span["origin"] == "auto.queue.ray"
for span in worker_transaction["spans"]:
assert span["op"] == "queue.task.ray"
assert span["origin"] == "auto.queue.ray"
@pytest.mark.forked
def test_ray_errors():
setup_sentry_with_logging_transport()
ray.init(
runtime_env={
"worker_process_setup_hook": setup_sentry_with_logging_transport,
"working_dir": "./",
}
)
@ray.remote
def example_task():
1 / 0
with sentry_sdk.start_transaction(op="task", name="ray test transaction"):
with pytest.raises(ZeroDivisionError):
future = example_task.remote()
ray.get(future)
job_id = future.job_id().hex()
# Read the worker log output containing the error
log_dir = "/tmp/ray/session_latest/logs/"
log_file = [
f
for f in os.listdir(log_dir)
if "worker" in f and job_id in f and f.endswith(".out")
][0]
with open(os.path.join(log_dir, log_file), "r") as file:
lines = file.readlines()
# parse error object from log line
error = json.loads(lines[4][:-1])
assert error["level"] == "error"
assert (
error["transaction"]
== "tests.integrations.ray.test_ray.test_ray_errors..example_task"
) # its in the worker, not the client thus not "ray test transaction"
assert error["exception"]["values"][0]["mechanism"]["type"] == "ray"
assert not error["exception"]["values"][0]["mechanism"]["handled"]
@pytest.mark.forked
def test_ray_actor():
setup_sentry()
ray.init(
runtime_env={
"worker_process_setup_hook": setup_sentry,
"working_dir": "./",
}
)
@ray.remote
class Counter:
def __init__(self):
self.n = 0
def increment(self):
with sentry_sdk.start_span(op="task", name="example task step"):
self.n += 1
return sentry_sdk.get_client().transport.envelopes
with sentry_sdk.start_transaction(op="task", name="ray test transaction"):
counter = Counter.remote()
worker_envelopes = ray.get(counter.increment.remote())
# Currently no transactions/spans are captured in actors
assert worker_envelopes == []
client_envelope = sentry_sdk.get_client().transport.envelopes[0]
client_transaction = client_envelope.get_transaction_event()
assert (
client_transaction["contexts"]["trace"]["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
)
for span in client_transaction["spans"]:
assert (
span["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
== client_transaction["contexts"]["trace"]["trace_id"]
)
sentry-python-2.18.0/tests/integrations/redis/ 0000775 0000000 0000000 00000000000 14712146540 0021427 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/redis/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0023537 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("redis")
sentry-python-2.18.0/tests/integrations/redis/asyncio/ 0000775 0000000 0000000 00000000000 14712146540 0023074 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/redis/asyncio/__init__.py 0000664 0000000 0000000 00000000071 14712146540 0025203 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("fakeredis.aioredis")
sentry-python-2.18.0/tests/integrations/redis/asyncio/test_redis_asyncio.py 0000664 0000000 0000000 00000006111 14712146540 0027337 0 ustar 00root root 0000000 0000000 import pytest
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.redis import RedisIntegration
from tests.conftest import ApproxDict
from fakeredis.aioredis import FakeRedis
@pytest.mark.asyncio
async def test_async_basic(sentry_init, capture_events):
sentry_init(integrations=[RedisIntegration()])
events = capture_events()
connection = FakeRedis()
await connection.get("foobar")
capture_message("hi")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb == {
"category": "redis",
"message": "GET 'foobar'",
"data": {
"db.operation": "GET",
"redis.key": "foobar",
"redis.command": "GET",
"redis.is_cluster": False,
},
"timestamp": crumb["timestamp"],
"type": "redis",
}
@pytest.mark.parametrize(
"is_transaction, send_default_pii, expected_first_ten",
[
(False, False, ["GET 'foo'", "SET 'bar' [Filtered]", "SET 'baz' [Filtered]"]),
(True, True, ["GET 'foo'", "SET 'bar' 1", "SET 'baz' 2"]),
],
)
@pytest.mark.asyncio
async def test_async_redis_pipeline(
sentry_init, capture_events, is_transaction, send_default_pii, expected_first_ten
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
connection = FakeRedis()
with start_transaction():
pipeline = connection.pipeline(transaction=is_transaction)
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
await pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"] == ApproxDict(
{
"redis.commands": {
"count": 3,
"first_ten": expected_first_ten,
},
SPANDATA.DB_SYSTEM: "redis",
SPANDATA.DB_NAME: "0",
SPANDATA.SERVER_ADDRESS: connection.connection_pool.connection_kwargs.get(
"host"
),
SPANDATA.SERVER_PORT: 6379,
}
)
assert span["tags"] == {
"redis.transaction": is_transaction,
"redis.is_cluster": False,
}
@pytest.mark.asyncio
async def test_async_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeRedis()
with start_transaction(name="custom_transaction"):
# default case
await connection.set("somekey", "somevalue")
# pipeline
pipeline = connection.pipeline(transaction=False)
pipeline.get("somekey")
pipeline.set("anotherkey", 1)
await pipeline.execute()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.redis"
sentry-python-2.18.0/tests/integrations/redis/cluster/ 0000775 0000000 0000000 00000000000 14712146540 0023110 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/redis/cluster/__init__.py 0000664 0000000 0000000 00000000064 14712146540 0025221 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("redis.cluster")
sentry-python-2.18.0/tests/integrations/redis/cluster/test_redis_cluster.py 0000664 0000000 0000000 00000012027 14712146540 0027372 0 ustar 00root root 0000000 0000000 import pytest
from sentry_sdk import capture_message
from sentry_sdk.consts import SPANDATA
from sentry_sdk.api import start_transaction
from sentry_sdk.integrations.redis import RedisIntegration
from tests.conftest import ApproxDict
import redis
@pytest.fixture(autouse=True)
def monkeypatch_rediscluster_class(reset_integrations):
pipeline_cls = redis.cluster.ClusterPipeline
redis.cluster.NodesManager.initialize = lambda *_, **__: None
redis.RedisCluster.command = lambda *_: []
redis.RedisCluster.pipeline = lambda *_, **__: pipeline_cls(None, None)
redis.RedisCluster.get_default_node = lambda *_, **__: redis.cluster.ClusterNode(
"localhost", 6379
)
pipeline_cls.execute = lambda *_, **__: None
redis.RedisCluster.execute_command = lambda *_, **__: []
def test_rediscluster_breadcrumb(sentry_init, capture_events):
sentry_init(integrations=[RedisIntegration()])
events = capture_events()
rc = redis.RedisCluster(host="localhost", port=6379)
rc.get("foobar")
capture_message("hi")
(event,) = events
crumbs = event["breadcrumbs"]["values"]
# on initializing a RedisCluster, a COMMAND call is made - this is not important for the test
# but must be accounted for
assert len(crumbs) in (1, 2)
assert len(crumbs) == 1 or crumbs[0]["message"] == "COMMAND"
crumb = crumbs[-1]
assert crumb == {
"category": "redis",
"message": "GET 'foobar'",
"data": {
"db.operation": "GET",
"redis.key": "foobar",
"redis.command": "GET",
"redis.is_cluster": True,
},
"timestamp": crumb["timestamp"],
"type": "redis",
}
@pytest.mark.parametrize(
"send_default_pii, description",
[
(False, "SET 'bar' [Filtered]"),
(True, "SET 'bar' 1"),
],
)
def test_rediscluster_basic(sentry_init, capture_events, send_default_pii, description):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
with start_transaction():
rc = redis.RedisCluster(host="localhost", port=6379)
rc.set("bar", 1)
(event,) = events
spans = event["spans"]
# on initializing a RedisCluster, a COMMAND call is made - this is not important for the test
# but must be accounted for
assert len(spans) in (1, 2)
assert len(spans) == 1 or spans[0]["description"] == "COMMAND"
span = spans[-1]
assert span["op"] == "db.redis"
assert span["description"] == description
assert span["data"] == ApproxDict(
{
SPANDATA.DB_SYSTEM: "redis",
# ClusterNode converts localhost to 127.0.0.1
SPANDATA.SERVER_ADDRESS: "127.0.0.1",
SPANDATA.SERVER_PORT: 6379,
}
)
assert span["tags"] == {
"db.operation": "SET",
"redis.command": "SET",
"redis.is_cluster": True,
"redis.key": "bar",
}
@pytest.mark.parametrize(
"send_default_pii, expected_first_ten",
[
(False, ["GET 'foo'", "SET 'bar' [Filtered]", "SET 'baz' [Filtered]"]),
(True, ["GET 'foo'", "SET 'bar' 1", "SET 'baz' 2"]),
],
)
def test_rediscluster_pipeline(
sentry_init, capture_events, send_default_pii, expected_first_ten
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
rc = redis.RedisCluster(host="localhost", port=6379)
with start_transaction():
pipeline = rc.pipeline()
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"] == ApproxDict(
{
"redis.commands": {
"count": 3,
"first_ten": expected_first_ten,
},
SPANDATA.DB_SYSTEM: "redis",
# ClusterNode converts localhost to 127.0.0.1
SPANDATA.SERVER_ADDRESS: "127.0.0.1",
SPANDATA.SERVER_PORT: 6379,
}
)
assert span["tags"] == {
"redis.transaction": False, # For Cluster, this is always False
"redis.is_cluster": True,
}
def test_rediscluster_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
rc = redis.RedisCluster(host="localhost", port=6379)
with start_transaction(name="custom_transaction"):
# default case
rc.set("somekey", "somevalue")
# pipeline
pipeline = rc.pipeline(transaction=False)
pipeline.get("somekey")
pipeline.set("anotherkey", 1)
pipeline.execute()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.redis"
sentry-python-2.18.0/tests/integrations/redis/cluster_asyncio/ 0000775 0000000 0000000 00000000000 14712146540 0024635 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/redis/cluster_asyncio/__init__.py 0000664 0000000 0000000 00000000074 14712146540 0026747 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("redis.asyncio.cluster")
sentry-python-2.18.0/tests/integrations/redis/cluster_asyncio/test_redis_cluster_asyncio.py 0000664 0000000 0000000 00000011524 14712146540 0032645 0 ustar 00root root 0000000 0000000 import pytest
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.redis import RedisIntegration
from tests.conftest import ApproxDict
from redis.asyncio import cluster
async def fake_initialize(*_, **__):
return None
async def fake_execute_command(*_, **__):
return []
async def fake_execute(*_, **__):
return None
@pytest.fixture(autouse=True)
def monkeypatch_rediscluster_asyncio_class(reset_integrations):
pipeline_cls = cluster.ClusterPipeline
cluster.NodesManager.initialize = fake_initialize
cluster.RedisCluster.get_default_node = lambda *_, **__: cluster.ClusterNode(
"localhost", 6379
)
cluster.RedisCluster.pipeline = lambda self, *_, **__: pipeline_cls(self)
pipeline_cls.execute = fake_execute
cluster.RedisCluster.execute_command = fake_execute_command
@pytest.mark.asyncio
async def test_async_breadcrumb(sentry_init, capture_events):
sentry_init(integrations=[RedisIntegration()])
events = capture_events()
connection = cluster.RedisCluster(host="localhost", port=6379)
await connection.get("foobar")
capture_message("hi")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb == {
"category": "redis",
"message": "GET 'foobar'",
"data": ApproxDict(
{
"db.operation": "GET",
"redis.key": "foobar",
"redis.command": "GET",
"redis.is_cluster": True,
}
),
"timestamp": crumb["timestamp"],
"type": "redis",
}
@pytest.mark.parametrize(
"send_default_pii, description",
[
(False, "SET 'bar' [Filtered]"),
(True, "SET 'bar' 1"),
],
)
@pytest.mark.asyncio
async def test_async_basic(sentry_init, capture_events, send_default_pii, description):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
connection = cluster.RedisCluster(host="localhost", port=6379)
with start_transaction():
await connection.set("bar", 1)
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == description
assert span["data"] == ApproxDict(
{
SPANDATA.DB_SYSTEM: "redis",
# ClusterNode converts localhost to 127.0.0.1
SPANDATA.SERVER_ADDRESS: "127.0.0.1",
SPANDATA.SERVER_PORT: 6379,
}
)
assert span["tags"] == {
"redis.is_cluster": True,
"db.operation": "SET",
"redis.command": "SET",
"redis.key": "bar",
}
@pytest.mark.parametrize(
"send_default_pii, expected_first_ten",
[
(False, ["GET 'foo'", "SET 'bar' [Filtered]", "SET 'baz' [Filtered]"]),
(True, ["GET 'foo'", "SET 'bar' 1", "SET 'baz' 2"]),
],
)
@pytest.mark.asyncio
async def test_async_redis_pipeline(
sentry_init, capture_events, send_default_pii, expected_first_ten
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
connection = cluster.RedisCluster(host="localhost", port=6379)
with start_transaction():
pipeline = connection.pipeline()
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
await pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"] == ApproxDict(
{
"redis.commands": {
"count": 3,
"first_ten": expected_first_ten,
},
SPANDATA.DB_SYSTEM: "redis",
# ClusterNode converts localhost to 127.0.0.1
SPANDATA.SERVER_ADDRESS: "127.0.0.1",
SPANDATA.SERVER_PORT: 6379,
}
)
assert span["tags"] == {
"redis.transaction": False,
"redis.is_cluster": True,
}
@pytest.mark.asyncio
async def test_async_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
connection = cluster.RedisCluster(host="localhost", port=6379)
with start_transaction(name="custom_transaction"):
# default case
await connection.set("somekey", "somevalue")
# pipeline
pipeline = connection.pipeline(transaction=False)
pipeline.get("somekey")
pipeline.set("anotherkey", 1)
await pipeline.execute()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.redis"
sentry-python-2.18.0/tests/integrations/redis/test_redis.py 0000664 0000000 0000000 00000022642 14712146540 0024154 0 ustar 00root root 0000000 0000000 from unittest import mock
import pytest
from fakeredis import FakeStrictRedis
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.redis import RedisIntegration
MOCK_CONNECTION_POOL = mock.MagicMock()
MOCK_CONNECTION_POOL.connection_kwargs = {
"host": "localhost",
"port": 63791,
"db": 1,
}
def test_basic(sentry_init, capture_events):
sentry_init(integrations=[RedisIntegration()])
events = capture_events()
connection = FakeStrictRedis()
connection.get("foobar")
capture_message("hi")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb == {
"category": "redis",
"message": "GET 'foobar'",
"data": {
"redis.key": "foobar",
"redis.command": "GET",
"redis.is_cluster": False,
"db.operation": "GET",
},
"timestamp": crumb["timestamp"],
"type": "redis",
}
@pytest.mark.parametrize(
"is_transaction, send_default_pii, expected_first_ten",
[
(False, False, ["GET 'foo'", "SET 'bar' [Filtered]", "SET 'baz' [Filtered]"]),
(True, True, ["GET 'foo'", "SET 'bar' 1", "SET 'baz' 2"]),
],
)
def test_redis_pipeline(
sentry_init, capture_events, is_transaction, send_default_pii, expected_first_ten
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
pipeline = connection.pipeline(transaction=is_transaction)
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
assert span["data"]["redis.commands"] == {
"count": 3,
"first_ten": expected_first_ten,
}
assert span["tags"] == {
"redis.transaction": is_transaction,
"redis.is_cluster": False,
}
def test_sensitive_data(sentry_init, capture_events):
# fakeredis does not support the AUTH command, so we need to mock it
with mock.patch(
"sentry_sdk.integrations.redis.utils._COMMANDS_INCLUDING_SENSITIVE_DATA",
["get"],
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
connection.get(
"this is super secret"
) # because fakeredis does not support AUTH we use GET instead
(event,) = events
spans = event["spans"]
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "GET [Filtered]"
def test_pii_data_redacted(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
connection.set("somekey1", "my secret string1")
connection.set("somekey2", "my secret string2")
connection.get("somekey2")
connection.delete("somekey1", "somekey2")
(event,) = events
spans = event["spans"]
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "SET 'somekey1' [Filtered]"
assert spans[1]["description"] == "SET 'somekey2' [Filtered]"
assert spans[2]["description"] == "GET 'somekey2'"
assert spans[3]["description"] == "DEL 'somekey1' [Filtered]"
def test_pii_data_sent(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
connection.set("somekey1", "my secret string1")
connection.set("somekey2", "my secret string2")
connection.get("somekey2")
connection.delete("somekey1", "somekey2")
(event,) = events
spans = event["spans"]
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "SET 'somekey1' 'my secret string1'"
assert spans[1]["description"] == "SET 'somekey2' 'my secret string2'"
assert spans[2]["description"] == "GET 'somekey2'"
assert spans[3]["description"] == "DEL 'somekey1' 'somekey2'"
def test_data_truncation(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=True,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
long_string = "a" * 100000
connection.set("somekey1", long_string)
short_string = "b" * 10
connection.set("somekey2", short_string)
(event,) = events
spans = event["spans"]
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "SET 'somekey1' '%s..." % (
long_string[: 1024 - len("...") - len("SET 'somekey1' '")],
)
assert spans[1]["description"] == "SET 'somekey2' '%s'" % (short_string,)
def test_data_truncation_custom(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration(max_data_size=30)],
traces_sample_rate=1.0,
send_default_pii=True,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction():
long_string = "a" * 100000
connection.set("somekey1", long_string)
short_string = "b" * 10
connection.set("somekey2", short_string)
(event,) = events
spans = event["spans"]
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "SET 'somekey1' '%s..." % (
long_string[: 30 - len("...") - len("SET 'somekey1' '")],
)
assert spans[1]["description"] == "SET 'somekey2' '%s'" % (short_string,)
def test_breadcrumbs(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration(max_data_size=30)],
send_default_pii=True,
)
events = capture_events()
connection = FakeStrictRedis()
long_string = "a" * 100000
connection.set("somekey1", long_string)
short_string = "b" * 10
connection.set("somekey2", short_string)
capture_message("hi")
(event,) = events
crumbs = event["breadcrumbs"]["values"]
assert crumbs[0] == {
"message": "SET 'somekey1' 'aaaaaaaaaaa...",
"type": "redis",
"category": "redis",
"data": {
"db.operation": "SET",
"redis.is_cluster": False,
"redis.command": "SET",
"redis.key": "somekey1",
},
"timestamp": crumbs[0]["timestamp"],
}
assert crumbs[1] == {
"message": "SET 'somekey2' 'bbbbbbbbbb'",
"type": "redis",
"category": "redis",
"data": {
"db.operation": "SET",
"redis.is_cluster": False,
"redis.command": "SET",
"redis.key": "somekey2",
},
"timestamp": crumbs[1]["timestamp"],
}
def test_db_connection_attributes_client(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
integrations=[RedisIntegration()],
)
events = capture_events()
with start_transaction():
connection = FakeStrictRedis(connection_pool=MOCK_CONNECTION_POOL)
connection.get("foobar")
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "GET 'foobar'"
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
assert span["data"][SPANDATA.DB_NAME] == "1"
assert span["data"][SPANDATA.SERVER_ADDRESS] == "localhost"
assert span["data"][SPANDATA.SERVER_PORT] == 63791
def test_db_connection_attributes_pipeline(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
integrations=[RedisIntegration()],
)
events = capture_events()
with start_transaction():
connection = FakeStrictRedis(connection_pool=MOCK_CONNECTION_POOL)
pipeline = connection.pipeline(transaction=False)
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
assert span["data"][SPANDATA.DB_NAME] == "1"
assert span["data"][SPANDATA.SERVER_ADDRESS] == "localhost"
assert span["data"][SPANDATA.SERVER_PORT] == 63791
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with start_transaction(name="custom_transaction"):
# default case
connection.set("somekey", "somevalue")
# pipeline
pipeline = connection.pipeline(transaction=False)
pipeline.get("somekey")
pipeline.set("anotherkey", 1)
pipeline.execute()
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
for span in event["spans"]:
assert span["origin"] == "auto.db.redis"
sentry-python-2.18.0/tests/integrations/redis/test_redis_cache_module.py 0000664 0000000 0000000 00000023302 14712146540 0026636 0 ustar 00root root 0000000 0000000 import uuid
import pytest
import fakeredis
from fakeredis import FakeStrictRedis
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
from sentry_sdk.utils import parse_version
import sentry_sdk
FAKEREDIS_VERSION = parse_version(fakeredis.__version__)
def test_no_cache_basic(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with sentry_sdk.start_transaction():
connection.get("mycachekey")
(event,) = events
spans = event["spans"]
assert len(spans) == 1
assert spans[0]["op"] == "db.redis"
def test_cache_basic(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["mycache"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with sentry_sdk.start_transaction():
connection.hget("mycachekey", "myfield")
connection.get("mycachekey")
connection.set("mycachekey1", "bla")
connection.setex("mycachekey2", 10, "blub")
connection.mget("mycachekey1", "mycachekey2")
(event,) = events
spans = event["spans"]
assert len(spans) == 9
# no cache support for hget command
assert spans[0]["op"] == "db.redis"
assert spans[0]["tags"]["redis.command"] == "HGET"
assert spans[1]["op"] == "cache.get"
assert spans[2]["op"] == "db.redis"
assert spans[2]["tags"]["redis.command"] == "GET"
assert spans[3]["op"] == "cache.put"
assert spans[4]["op"] == "db.redis"
assert spans[4]["tags"]["redis.command"] == "SET"
assert spans[5]["op"] == "cache.put"
assert spans[6]["op"] == "db.redis"
assert spans[6]["tags"]["redis.command"] == "SETEX"
assert spans[7]["op"] == "cache.get"
assert spans[8]["op"] == "db.redis"
assert spans[8]["tags"]["redis.command"] == "MGET"
def test_cache_keys(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["bla", "blub"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with sentry_sdk.start_transaction():
connection.get("somethingelse")
connection.get("blub")
connection.get("blubkeything")
connection.get("bl")
(event,) = events
spans = event["spans"]
assert len(spans) == 6
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "GET 'somethingelse'"
assert spans[1]["op"] == "cache.get"
assert spans[1]["description"] == "blub"
assert spans[2]["op"] == "db.redis"
assert spans[2]["description"] == "GET 'blub'"
assert spans[3]["op"] == "cache.get"
assert spans[3]["description"] == "blubkeything"
assert spans[4]["op"] == "db.redis"
assert spans[4]["description"] == "GET 'blubkeything'"
assert spans[5]["op"] == "db.redis"
assert spans[5]["description"] == "GET 'bl'"
def test_cache_data(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["mycache"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis(host="mycacheserver.io", port=6378)
with sentry_sdk.start_transaction():
connection.get("mycachekey")
connection.set("mycachekey", "事实胜于雄辩")
connection.get("mycachekey")
(event,) = events
spans = event["spans"]
assert len(spans) == 6
assert spans[0]["op"] == "cache.get"
assert spans[0]["description"] == "mycachekey"
assert spans[0]["data"]["cache.key"] == [
"mycachekey",
]
assert spans[0]["data"]["cache.hit"] == False # noqa: E712
assert "cache.item_size" not in spans[0]["data"]
# very old fakeredis can not handle port and/or host.
# only applicable for Redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[0]["data"]
else:
assert spans[0]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[0]["data"]
else:
assert spans[0]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[1]["op"] == "db.redis" # we ignore db spans in this test.
assert spans[2]["op"] == "cache.put"
assert spans[2]["description"] == "mycachekey"
assert spans[2]["data"]["cache.key"] == [
"mycachekey",
]
assert "cache.hit" not in spans[1]["data"]
assert spans[2]["data"]["cache.item_size"] == 18
# very old fakeredis can not handle port.
# only used with redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[2]["data"]
else:
assert spans[2]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[2]["data"]
else:
assert spans[2]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[3]["op"] == "db.redis" # we ignore db spans in this test.
assert spans[4]["op"] == "cache.get"
assert spans[4]["description"] == "mycachekey"
assert spans[4]["data"]["cache.key"] == [
"mycachekey",
]
assert spans[4]["data"]["cache.hit"] == True # noqa: E712
assert spans[4]["data"]["cache.item_size"] == 18
# very old fakeredis can not handle port.
# only used with redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[4]["data"]
else:
assert spans[4]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[4]["data"]
else:
assert spans[4]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[5]["op"] == "db.redis" # we ignore db spans in this test.
def test_cache_prefixes(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["yes"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeStrictRedis()
with sentry_sdk.start_transaction():
connection.mget("yes", "no")
connection.mget("no", 1, "yes")
connection.mget("no", "yes.1", "yes.2")
connection.mget("no.1", "no.2", "no.3")
connection.mget("no.1", "no.2", "no.actually.yes")
connection.mget(b"no.3", b"yes.5")
connection.mget(uuid.uuid4().bytes)
connection.mget(uuid.uuid4().bytes, "yes")
(event,) = events
spans = event["spans"]
assert len(spans) == 13 # 8 db spans + 5 cache spans
cache_spans = [span for span in spans if span["op"] == "cache.get"]
assert len(cache_spans) == 5
assert cache_spans[0]["description"] == "yes, no"
assert cache_spans[1]["description"] == "no, 1, yes"
assert cache_spans[2]["description"] == "no, yes.1, yes.2"
assert cache_spans[3]["description"] == "no.3, yes.5"
assert cache_spans[4]["description"] == ", yes"
@pytest.mark.parametrize(
"method_name,args,kwargs,expected_key",
[
(None, None, None, None),
("", None, None, None),
("set", ["bla", "valuebla"], None, ("bla",)),
("setex", ["bla", 10, "valuebla"], None, ("bla",)),
("get", ["bla"], None, ("bla",)),
("mget", ["bla", "blub", "foo"], None, ("bla", "blub", "foo")),
("set", [b"bla", "valuebla"], None, (b"bla",)),
("setex", [b"bla", 10, "valuebla"], None, (b"bla",)),
("get", [b"bla"], None, (b"bla",)),
("mget", [b"bla", "blub", "foo"], None, (b"bla", "blub", "foo")),
("not-important", None, {"something": "bla"}, None),
("not-important", None, {"key": None}, None),
("not-important", None, {"key": "bla"}, ("bla",)),
("not-important", None, {"key": b"bla"}, (b"bla",)),
("not-important", None, {"key": []}, None),
(
"not-important",
None,
{
"key": [
"bla",
]
},
("bla",),
),
(
"not-important",
None,
{"key": [b"bla", "blub", "foo"]},
(b"bla", "blub", "foo"),
),
(
"not-important",
None,
{"key": b"\x00c\x0f\xeaC\xe1L\x1c\xbff\xcb\xcc\xc1\xed\xc6\t"},
(b"\x00c\x0f\xeaC\xe1L\x1c\xbff\xcb\xcc\xc1\xed\xc6\t",),
),
(
"get",
[b"\x00c\x0f\xeaC\xe1L\x1c\xbff\xcb\xcc\xc1\xed\xc6\t"],
None,
(b"\x00c\x0f\xeaC\xe1L\x1c\xbff\xcb\xcc\xc1\xed\xc6\t",),
),
(
"get",
[123],
None,
(123,),
),
],
)
def test_get_safe_key(method_name, args, kwargs, expected_key):
assert _get_safe_key(method_name, args, kwargs) == expected_key
@pytest.mark.parametrize(
"key,expected_key",
[
(None, ""),
(("bla",), "bla"),
(("bla", "blub", "foo"), "bla, blub, foo"),
((b"bla",), "bla"),
((b"bla", "blub", "foo"), "bla, blub, foo"),
(
[
"bla",
],
"bla",
),
(["bla", "blub", "foo"], "bla, blub, foo"),
([uuid.uuid4().bytes], ""),
({"key1": 1, "key2": 2}, "key1, key2"),
(1, "1"),
([1, 2, 3, b"hello"], "1, 2, 3, hello"),
],
)
def test_key_as_string(key, expected_key):
assert _key_as_string(key) == expected_key
sentry-python-2.18.0/tests/integrations/redis/test_redis_cache_module_async.py 0000664 0000000 0000000 00000013225 14712146540 0030036 0 ustar 00root root 0000000 0000000 import pytest
try:
import fakeredis
from fakeredis.aioredis import FakeRedis as FakeRedisAsync
except ModuleNotFoundError:
FakeRedisAsync = None
if FakeRedisAsync is None:
pytest.skip(
"Skipping tests because fakeredis.aioredis not available",
allow_module_level=True,
)
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.utils import parse_version
import sentry_sdk
FAKEREDIS_VERSION = parse_version(fakeredis.__version__)
@pytest.mark.asyncio
async def test_no_cache_basic(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeRedisAsync()
with sentry_sdk.start_transaction():
await connection.get("myasynccachekey")
(event,) = events
spans = event["spans"]
assert len(spans) == 1
assert spans[0]["op"] == "db.redis"
@pytest.mark.asyncio
async def test_cache_basic(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["myasynccache"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeRedisAsync()
with sentry_sdk.start_transaction():
await connection.get("myasynccachekey")
(event,) = events
spans = event["spans"]
assert len(spans) == 2
assert spans[0]["op"] == "cache.get"
assert spans[1]["op"] == "db.redis"
@pytest.mark.asyncio
async def test_cache_keys(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["abla", "ablub"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeRedisAsync()
with sentry_sdk.start_transaction():
await connection.get("asomethingelse")
await connection.get("ablub")
await connection.get("ablubkeything")
await connection.get("abl")
(event,) = events
spans = event["spans"]
assert len(spans) == 6
assert spans[0]["op"] == "db.redis"
assert spans[0]["description"] == "GET 'asomethingelse'"
assert spans[1]["op"] == "cache.get"
assert spans[1]["description"] == "ablub"
assert spans[2]["op"] == "db.redis"
assert spans[2]["description"] == "GET 'ablub'"
assert spans[3]["op"] == "cache.get"
assert spans[3]["description"] == "ablubkeything"
assert spans[4]["op"] == "db.redis"
assert spans[4]["description"] == "GET 'ablubkeything'"
assert spans[5]["op"] == "db.redis"
assert spans[5]["description"] == "GET 'abl'"
@pytest.mark.asyncio
async def test_cache_data(sentry_init, capture_events):
sentry_init(
integrations=[
RedisIntegration(
cache_prefixes=["myasynccache"],
),
],
traces_sample_rate=1.0,
)
events = capture_events()
connection = FakeRedisAsync(host="mycacheserver.io", port=6378)
with sentry_sdk.start_transaction():
await connection.get("myasynccachekey")
await connection.set("myasynccachekey", "事实胜于雄辩")
await connection.get("myasynccachekey")
(event,) = events
spans = event["spans"]
assert len(spans) == 6
assert spans[0]["op"] == "cache.get"
assert spans[0]["description"] == "myasynccachekey"
assert spans[0]["data"]["cache.key"] == [
"myasynccachekey",
]
assert spans[0]["data"]["cache.hit"] == False # noqa: E712
assert "cache.item_size" not in spans[0]["data"]
# very old fakeredis can not handle port and/or host.
# only applicable for Redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[0]["data"]
else:
assert spans[0]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[0]["data"]
else:
assert spans[0]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[1]["op"] == "db.redis" # we ignore db spans in this test.
assert spans[2]["op"] == "cache.put"
assert spans[2]["description"] == "myasynccachekey"
assert spans[2]["data"]["cache.key"] == [
"myasynccachekey",
]
assert "cache.hit" not in spans[1]["data"]
assert spans[2]["data"]["cache.item_size"] == 18
# very old fakeredis can not handle port.
# only used with redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[2]["data"]
else:
assert spans[2]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[2]["data"]
else:
assert spans[2]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[3]["op"] == "db.redis" # we ignore db spans in this test.
assert spans[4]["op"] == "cache.get"
assert spans[4]["description"] == "myasynccachekey"
assert spans[4]["data"]["cache.key"] == [
"myasynccachekey",
]
assert spans[4]["data"]["cache.hit"] == True # noqa: E712
assert spans[4]["data"]["cache.item_size"] == 18
# very old fakeredis can not handle port.
# only used with redis v3
if FAKEREDIS_VERSION <= (2, 7, 1):
assert "network.peer.port" not in spans[4]["data"]
else:
assert spans[4]["data"]["network.peer.port"] == 6378
if FAKEREDIS_VERSION <= (1, 7, 1):
assert "network.peer.address" not in spans[4]["data"]
else:
assert spans[4]["data"]["network.peer.address"] == "mycacheserver.io"
assert spans[5]["op"] == "db.redis" # we ignore db spans in this test.
sentry-python-2.18.0/tests/integrations/redis_py_cluster_legacy/ 0000775 0000000 0000000 00000000000 14712146540 0025224 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/redis_py_cluster_legacy/__init__.py 0000664 0000000 0000000 00000000063 14712146540 0027334 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("rediscluster")
sentry-python-2.18.0/tests/integrations/redis_py_cluster_legacy/test_redis_py_cluster_legacy.py 0000664 0000000 0000000 00000011552 14712146540 0033544 0 ustar 00root root 0000000 0000000 from unittest import mock
import pytest
import rediscluster
from sentry_sdk import capture_message
from sentry_sdk.api import start_transaction
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.redis import RedisIntegration
from tests.conftest import ApproxDict
MOCK_CONNECTION_POOL = mock.MagicMock()
MOCK_CONNECTION_POOL.connection_kwargs = {
"host": "localhost",
"port": 63791,
"db": 1,
}
rediscluster_classes = [rediscluster.RedisCluster]
if hasattr(rediscluster, "StrictRedisCluster"):
rediscluster_classes.append(rediscluster.StrictRedisCluster)
@pytest.fixture(autouse=True)
def monkeypatch_rediscluster_classes(reset_integrations):
try:
pipeline_cls = rediscluster.pipeline.ClusterPipeline
except AttributeError:
pipeline_cls = rediscluster.StrictClusterPipeline
rediscluster.RedisCluster.pipeline = lambda *_, **__: pipeline_cls(
connection_pool=MOCK_CONNECTION_POOL
)
pipeline_cls.execute = lambda *_, **__: None
for cls in rediscluster_classes:
cls.execute_command = lambda *_, **__: None
@pytest.mark.parametrize("rediscluster_cls", rediscluster_classes)
def test_rediscluster_basic(rediscluster_cls, sentry_init, capture_events):
sentry_init(integrations=[RedisIntegration()])
events = capture_events()
rc = rediscluster_cls(connection_pool=MOCK_CONNECTION_POOL)
rc.get("foobar")
capture_message("hi")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb == {
"category": "redis",
"message": "GET 'foobar'",
"data": ApproxDict(
{
"db.operation": "GET",
"redis.key": "foobar",
"redis.command": "GET",
"redis.is_cluster": True,
}
),
"timestamp": crumb["timestamp"],
"type": "redis",
}
@pytest.mark.parametrize(
"send_default_pii, expected_first_ten",
[
(False, ["GET 'foo'", "SET 'bar' [Filtered]", "SET 'baz' [Filtered]"]),
(True, ["GET 'foo'", "SET 'bar' 1", "SET 'baz' 2"]),
],
)
def test_rediscluster_pipeline(
sentry_init, capture_events, send_default_pii, expected_first_ten
):
sentry_init(
integrations=[RedisIntegration()],
traces_sample_rate=1.0,
send_default_pii=send_default_pii,
)
events = capture_events()
rc = rediscluster.RedisCluster(connection_pool=MOCK_CONNECTION_POOL)
with start_transaction():
pipeline = rc.pipeline()
pipeline.get("foo")
pipeline.set("bar", 1)
pipeline.set("baz", 2)
pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"] == ApproxDict(
{
"redis.commands": {
"count": 3,
"first_ten": expected_first_ten,
},
SPANDATA.DB_SYSTEM: "redis",
SPANDATA.DB_NAME: "1",
SPANDATA.SERVER_ADDRESS: "localhost",
SPANDATA.SERVER_PORT: 63791,
}
)
assert span["tags"] == {
"redis.transaction": False, # For Cluster, this is always False
"redis.is_cluster": True,
}
@pytest.mark.parametrize("rediscluster_cls", rediscluster_classes)
def test_db_connection_attributes_client(sentry_init, capture_events, rediscluster_cls):
sentry_init(
traces_sample_rate=1.0,
integrations=[RedisIntegration()],
)
events = capture_events()
rc = rediscluster_cls(connection_pool=MOCK_CONNECTION_POOL)
with start_transaction():
rc.get("foobar")
(event,) = events
(span,) = event["spans"]
assert span["data"] == ApproxDict(
{
SPANDATA.DB_SYSTEM: "redis",
SPANDATA.DB_NAME: "1",
SPANDATA.SERVER_ADDRESS: "localhost",
SPANDATA.SERVER_PORT: 63791,
}
)
@pytest.mark.parametrize("rediscluster_cls", rediscluster_classes)
def test_db_connection_attributes_pipeline(
sentry_init, capture_events, rediscluster_cls
):
sentry_init(
traces_sample_rate=1.0,
integrations=[RedisIntegration()],
)
events = capture_events()
rc = rediscluster.RedisCluster(connection_pool=MOCK_CONNECTION_POOL)
with start_transaction():
pipeline = rc.pipeline()
pipeline.get("foo")
pipeline.execute()
(event,) = events
(span,) = event["spans"]
assert span["op"] == "db.redis"
assert span["description"] == "redis.pipeline.execute"
assert span["data"] == ApproxDict(
{
"redis.commands": {
"count": 1,
"first_ten": ["GET 'foo'"],
},
SPANDATA.DB_SYSTEM: "redis",
SPANDATA.DB_NAME: "1",
SPANDATA.SERVER_ADDRESS: "localhost",
SPANDATA.SERVER_PORT: 63791,
}
)
sentry-python-2.18.0/tests/integrations/requests/ 0000775 0000000 0000000 00000000000 14712146540 0022174 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/requests/__init__.py 0000664 0000000 0000000 00000000057 14712146540 0024307 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("requests")
sentry-python-2.18.0/tests/integrations/requests/test_requests.py 0000664 0000000 0000000 00000003667 14712146540 0025474 0 ustar 00root root 0000000 0000000 from unittest import mock
import pytest
import requests
import responses
from sentry_sdk import capture_message
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.stdlib import StdlibIntegration
from tests.conftest import ApproxDict
def test_crumb_capture(sentry_init, capture_events):
sentry_init(integrations=[StdlibIntegration()])
url = "http://example.com/"
responses.add(responses.GET, url, status=200)
events = capture_events()
response = requests.get(url)
capture_message("Testing!")
(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == ApproxDict(
{
"url": url,
SPANDATA.HTTP_METHOD: "GET",
SPANDATA.HTTP_FRAGMENT: "",
SPANDATA.HTTP_QUERY: "",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
"reason": response.reason,
}
)
@pytest.mark.tests_internal_exceptions
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
sentry_init(integrations=[StdlibIntegration()])
url = "https://example.com"
responses.add(responses.GET, url, status=200)
events = capture_events()
with mock.patch(
"sentry_sdk.integrations.stdlib.parse_url",
side_effect=ValueError,
):
response = requests.get(url)
capture_message("Testing!")
(event,) = events
assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict(
{
SPANDATA.HTTP_METHOD: "GET",
SPANDATA.HTTP_STATUS_CODE: response.status_code,
"reason": response.reason,
# no url related data
}
)
assert "url" not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"]
assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"]
sentry-python-2.18.0/tests/integrations/rq/ 0000775 0000000 0000000 00000000000 14712146540 0020743 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/rq/__init__.py 0000664 0000000 0000000 00000000051 14712146540 0023050 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("rq")
sentry-python-2.18.0/tests/integrations/rq/test_rq.py 0000664 0000000 0000000 00000021662 14712146540 0023005 0 ustar 00root root 0000000 0000000 from unittest import mock
import pytest
import rq
from fakeredis import FakeStrictRedis
import sentry_sdk
from sentry_sdk import start_transaction
from sentry_sdk.integrations.rq import RqIntegration
from sentry_sdk.utils import parse_version
@pytest.fixture(autouse=True)
def _patch_rq_get_server_version(monkeypatch):
"""
Patch RQ lower than 1.5.1 to work with fakeredis.
https://github.com/jamesls/fakeredis/issues/273
"""
try:
from distutils.version import StrictVersion
except ImportError:
return
if parse_version(rq.VERSION) <= (1, 5, 1):
for k in (
"rq.job.Job.get_redis_server_version",
"rq.worker.Worker.get_redis_server_version",
):
try:
monkeypatch.setattr(k, lambda _: StrictVersion("4.0.0"))
except AttributeError:
# old RQ Job/Worker doesn't have a get_redis_server_version attr
pass
def crashing_job(foo):
1 / 0
def chew_up_shoes(dog, human, shoes):
raise Exception("{}!! Why did you eat {}'s {}??".format(dog, human, shoes))
def do_trick(dog, trick):
return "{}, can you {}? Good dog!".format(dog, trick)
def test_basic(sentry_init, capture_events):
sentry_init(integrations=[RqIntegration()])
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(crashing_job, foo=42)
worker.work(burst=True)
(event,) = events
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
assert exception["mechanism"]["type"] == "rq"
assert exception["stacktrace"]["frames"][-1]["vars"]["foo"] == "42"
assert event["transaction"] == "tests.integrations.rq.test_rq.crashing_job"
extra = event["extra"]["rq-job"]
assert extra["args"] == []
assert extra["kwargs"] == {"foo": 42}
assert extra["description"] == "tests.integrations.rq.test_rq.crashing_job(foo=42)"
assert extra["func"] == "tests.integrations.rq.test_rq.crashing_job"
assert "job_id" in extra
assert "enqueued_at" in extra
# older versions don't persist started_at correctly
if tuple(map(int, rq.VERSION.split("."))) >= (0, 9):
assert "started_at" in extra
def test_transport_shutdown(sentry_init, capture_events_forksafe):
sentry_init(integrations=[RqIntegration()])
events = capture_events_forksafe()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.Worker([queue], connection=queue.connection)
queue.enqueue(crashing_job, foo=42)
worker.work(burst=True)
event = events.read_event()
events.read_flush()
(exception,) = event["exception"]["values"]
assert exception["type"] == "ZeroDivisionError"
def test_transaction_with_error(
sentry_init, capture_events, DictionaryContaining # noqa:N803
):
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(chew_up_shoes, "Charlie", "Katie", shoes="flip-flops")
worker.work(burst=True)
error_event, envelope = events
assert error_event["transaction"] == "tests.integrations.rq.test_rq.chew_up_shoes"
assert error_event["contexts"]["trace"]["op"] == "queue.task.rq"
assert error_event["exception"]["values"][0]["type"] == "Exception"
assert (
error_event["exception"]["values"][0]["value"]
== "Charlie!! Why did you eat Katie's flip-flops??"
)
assert envelope["type"] == "transaction"
assert envelope["contexts"]["trace"] == error_event["contexts"]["trace"]
assert envelope["transaction"] == error_event["transaction"]
assert envelope["extra"]["rq-job"] == DictionaryContaining(
{
"args": ["Charlie", "Katie"],
"kwargs": {"shoes": "flip-flops"},
"func": "tests.integrations.rq.test_rq.chew_up_shoes",
"description": "tests.integrations.rq.test_rq.chew_up_shoes('Charlie', 'Katie', shoes='flip-flops')",
}
)
def test_error_has_trace_context_if_tracing_disabled(
sentry_init,
capture_events,
):
sentry_init(integrations=[RqIntegration()])
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(crashing_job, foo=None)
worker.work(burst=True)
(error_event,) = events
assert error_event["contexts"]["trace"]
def test_tracing_enabled(
sentry_init,
capture_events,
):
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
with start_transaction(op="rq transaction") as transaction:
queue.enqueue(crashing_job, foo=None)
worker.work(burst=True)
error_event, envelope, _ = events
assert error_event["transaction"] == "tests.integrations.rq.test_rq.crashing_job"
assert error_event["contexts"]["trace"]["trace_id"] == transaction.trace_id
assert envelope["contexts"]["trace"] == error_event["contexts"]["trace"]
def test_tracing_disabled(
sentry_init,
capture_events,
):
sentry_init(integrations=[RqIntegration()])
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
scope = sentry_sdk.get_isolation_scope()
queue.enqueue(crashing_job, foo=None)
worker.work(burst=True)
(error_event,) = events
assert error_event["transaction"] == "tests.integrations.rq.test_rq.crashing_job"
assert (
error_event["contexts"]["trace"]["trace_id"]
== scope._propagation_context.trace_id
)
def test_transaction_no_error(
sentry_init, capture_events, DictionaryContaining # noqa:N803
):
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(do_trick, "Maisey", trick="kangaroo")
worker.work(burst=True)
envelope = events[0]
assert envelope["type"] == "transaction"
assert envelope["contexts"]["trace"]["op"] == "queue.task.rq"
assert envelope["transaction"] == "tests.integrations.rq.test_rq.do_trick"
assert envelope["extra"]["rq-job"] == DictionaryContaining(
{
"args": ["Maisey"],
"kwargs": {"trick": "kangaroo"},
"func": "tests.integrations.rq.test_rq.do_trick",
"description": "tests.integrations.rq.test_rq.do_trick('Maisey', trick='kangaroo')",
}
)
def test_traces_sampler_gets_correct_values_in_sampling_context(
sentry_init, DictionaryContaining, ObjectDescribedBy # noqa:N803
):
traces_sampler = mock.Mock(return_value=True)
sentry_init(integrations=[RqIntegration()], traces_sampler=traces_sampler)
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(do_trick, "Bodhi", trick="roll over")
worker.work(burst=True)
traces_sampler.assert_any_call(
DictionaryContaining(
{
"rq_job": ObjectDescribedBy(
type=rq.job.Job,
attrs={
"description": "tests.integrations.rq.test_rq.do_trick('Bodhi', trick='roll over')",
"result": "Bodhi, can you roll over? Good dog!",
"func_name": "tests.integrations.rq.test_rq.do_trick",
"args": ("Bodhi",),
"kwargs": {"trick": "roll over"},
},
),
}
)
)
@pytest.mark.skipif(
parse_version(rq.__version__) < (1, 5), reason="At least rq-1.5 required"
)
@pytest.mark.skipif(
parse_version(rq.__version__) >= (2,),
reason="Test broke in RQ 2.0. Investigate and fix. "
"See https://github.com/getsentry/sentry-python/issues/3707.",
)
def test_job_with_retries(sentry_init, capture_events):
sentry_init(integrations=[RqIntegration()])
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(crashing_job, foo=42, retry=rq.Retry(max=1))
worker.work(burst=True)
assert len(events) == 1
def test_span_origin(sentry_init, capture_events):
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
events = capture_events()
queue = rq.Queue(connection=FakeStrictRedis())
worker = rq.SimpleWorker([queue], connection=queue.connection)
queue.enqueue(do_trick, "Maisey", trick="kangaroo")
worker.work(burst=True)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.queue.rq"
sentry-python-2.18.0/tests/integrations/sanic/ 0000775 0000000 0000000 00000000000 14712146540 0021416 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/sanic/__init__.py 0000664 0000000 0000000 00000000054 14712146540 0023526 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("sanic")
sentry-python-2.18.0/tests/integrations/sanic/test_sanic.py 0000664 0000000 0000000 00000033213 14712146540 0024126 0 ustar 00root root 0000000 0000000 import asyncio
import contextlib
import os
import random
import sys
from unittest.mock import Mock
import pytest
import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations.sanic import SanicIntegration
from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT, TRANSACTION_SOURCE_URL
from sanic import Sanic, request, response, __version__ as SANIC_VERSION_RAW
from sanic.response import HTTPResponse
from sanic.exceptions import SanicException
try:
from sanic_testing import TestManager
except ImportError:
TestManager = None
try:
from sanic_testing.reusable import ReusableClient
except ImportError:
ReusableClient = None
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterable, Container
from typing import Any, Optional
SANIC_VERSION = tuple(map(int, SANIC_VERSION_RAW.split(".")))
PERFORMANCE_SUPPORTED = SANIC_VERSION >= (21, 9)
@pytest.fixture
def app():
if SANIC_VERSION < (19,):
"""
Older Sanic versions 0.8 and 18 bind to the same fixed port which
creates problems when we run tests concurrently.
"""
old_test_client = Sanic.test_client.__get__
def new_test_client(self):
client = old_test_client(self, Sanic)
client.port += os.getpid() % 100
return client
Sanic.test_client = property(new_test_client)
if SANIC_VERSION >= (20, 12) and SANIC_VERSION < (22, 6):
# Some builds (20.12.0 intruduced and 22.6.0 removed again) have a feature where the instance is stored in an internal class
# registry for later retrieval, and so add register=False to disable that
sanic_app = Sanic("Test", register=False)
else:
sanic_app = Sanic("Test")
if TestManager is not None:
TestManager(sanic_app)
@sanic_app.route("/message")
def hi(request):
capture_message("hi")
return response.text("ok")
@sanic_app.route("/message/")
def hi_with_id(request, message_id):
capture_message("hi with id")
return response.text("ok with id")
@sanic_app.route("/500")
def fivehundred(_):
1 / 0
return sanic_app
def get_client(app):
@contextlib.contextmanager
def simple_client(app):
yield app.test_client
if ReusableClient is not None:
return ReusableClient(app)
else:
return simple_client(app)
def test_request_data(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()
c = get_client(app)
with c as client:
_, response = client.get("/message?foo=bar")
assert response.status == 200
(event,) = events
assert event["transaction"] == "hi"
assert event["request"]["env"] == {"REMOTE_ADDR": ""}
assert set(event["request"]["headers"]) >= {
"accept",
"accept-encoding",
"host",
"user-agent",
}
assert event["request"]["query_string"] == "foo=bar"
assert event["request"]["url"].endswith("/message")
assert event["request"]["method"] == "GET"
# Assert that state is not leaked
events.clear()
capture_message("foo")
(event,) = events
assert "request" not in event
assert "transaction" not in event
@pytest.mark.parametrize(
"url,expected_transaction,expected_source",
[
("/message", "hi", "component"),
("/message/123456", "hi_with_id", "component"),
],
)
def test_transaction_name(
sentry_init, app, capture_events, url, expected_transaction, expected_source
):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()
c = get_client(app)
with c as client:
_, response = client.get(url)
assert response.status == 200
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}
def test_errors(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()
@app.route("/error")
def myerror(request):
raise ValueError("oh no")
c = get_client(app)
with c as client:
_, response = client.get("/error")
assert response.status == 500
(event,) = events
assert event["transaction"] == "myerror"
(exception,) = event["exception"]["values"]
assert exception["type"] == "ValueError"
assert exception["value"] == "oh no"
assert any(
frame["filename"].endswith("test_sanic.py")
for frame in exception["stacktrace"]["frames"]
)
def test_bad_request_not_captured(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()
@app.route("/")
def index(request):
raise SanicException("...", status_code=400)
c = get_client(app)
with c as client:
_, response = client.get("/")
assert response.status == 400
assert not events
def test_error_in_errorhandler(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()])
events = capture_events()
@app.route("/error")
def myerror(request):
raise ValueError("oh no")
@app.exception(ValueError)
def myhandler(request, exception):
1 / 0
c = get_client(app)
with c as client:
_, response = client.get("/error")
assert response.status == 500
event1, event2 = events
(exception,) = event1["exception"]["values"]
assert exception["type"] == "ValueError"
assert any(
frame["filename"].endswith("test_sanic.py")
for frame in exception["stacktrace"]["frames"]
)
exception = event2["exception"]["values"][-1]
assert exception["type"] == "ZeroDivisionError"
assert any(
frame["filename"].endswith("test_sanic.py")
for frame in exception["stacktrace"]["frames"]
)
def test_concurrency(sentry_init, app):
"""
Make sure we instrument Sanic in a way where request data does not leak
between request handlers. This test also implicitly tests our concept of
how async code should be instrumented, so if it breaks it likely has
ramifications for other async integrations and async usercode.
We directly call the request handler instead of using Sanic's test client
because that's the only way we could reproduce leakage with such a low
amount of concurrent tasks.
"""
sentry_init(integrations=[SanicIntegration()])
@app.route("/context-check/")
async def context_check(request, i):
scope = sentry_sdk.get_isolation_scope()
scope.set_tag("i", i)
await asyncio.sleep(random.random())
scope = sentry_sdk.get_isolation_scope()
assert scope._tags["i"] == i
return response.text("ok")
async def task(i):
responses = []
kwargs = {
"url_bytes": "http://localhost/context-check/{i}".format(i=i).encode(
"ascii"
),
"headers": {},
"version": "1.1",
"method": "GET",
"transport": None,
}
if SANIC_VERSION >= (19,):
kwargs["app"] = app
if SANIC_VERSION >= (21, 3):
class MockAsyncStreamer:
def __init__(self, request_body):
self.request_body = request_body
self.iter = iter(self.request_body)
if SANIC_VERSION >= (21, 12):
self.response = None
self.stage = Mock()
else:
self.response = b"success"
def respond(self, response):
responses.append(response)
patched_response = HTTPResponse()
return patched_response
def __aiter__(self):
return self
async def __anext__(self):
try:
return next(self.iter)
except StopIteration:
raise StopAsyncIteration
patched_request = request.Request(**kwargs)
patched_request.stream = MockAsyncStreamer([b"hello", b"foo"])
if SANIC_VERSION >= (21, 9):
await app.dispatch(
"http.lifecycle.request",
context={"request": patched_request},
inline=True,
)
await app.handle_request(
patched_request,
)
else:
await app.handle_request(
request.Request(**kwargs),
write_callback=responses.append,
stream_callback=responses.append,
)
(r,) = responses
assert r.status == 200
async def runner():
if SANIC_VERSION >= (21, 3):
if SANIC_VERSION >= (21, 9):
await app._startup()
else:
try:
app.router.reset()
app.router.finalize()
except AttributeError:
...
await asyncio.gather(*(task(i) for i in range(1000)))
if sys.version_info < (3, 7):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(runner())
else:
asyncio.run(runner())
scope = sentry_sdk.get_isolation_scope()
assert not scope._tags
class TransactionTestConfig:
"""
Data class to store configurations for each performance transaction test run, including
both the inputs and relevant expected results.
"""
def __init__(
self,
integration_args,
url,
expected_status,
expected_transaction_name,
expected_source=None,
):
# type: (Iterable[Optional[Container[int]]], str, int, Optional[str], Optional[str]) -> None
"""
expected_transaction_name of None indicates we expect to not receive a transaction
"""
self.integration_args = integration_args
self.url = url
self.expected_status = expected_status
self.expected_transaction_name = expected_transaction_name
self.expected_source = expected_source
@pytest.mark.skipif(
not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version"
)
@pytest.mark.parametrize(
"test_config",
[
TransactionTestConfig(
# Transaction for successful page load
integration_args=(),
url="/message",
expected_status=200,
expected_transaction_name="hi",
expected_source=TRANSACTION_SOURCE_COMPONENT,
),
TransactionTestConfig(
# Transaction still recorded when we have an internal server error
integration_args=(),
url="/500",
expected_status=500,
expected_transaction_name="fivehundred",
expected_source=TRANSACTION_SOURCE_COMPONENT,
),
TransactionTestConfig(
# By default, no transaction when we have a 404 error
integration_args=(),
url="/404",
expected_status=404,
expected_transaction_name=None,
),
TransactionTestConfig(
# With no ignored HTTP statuses, we should get transactions for 404 errors
integration_args=(None,),
url="/404",
expected_status=404,
expected_transaction_name="/404",
expected_source=TRANSACTION_SOURCE_URL,
),
TransactionTestConfig(
# Transaction can be suppressed for other HTTP statuses, too, by passing config to the integration
integration_args=({200},),
url="/message",
expected_status=200,
expected_transaction_name=None,
),
],
)
def test_transactions(test_config, sentry_init, app, capture_events):
# type: (TransactionTestConfig, Any, Any, Any) -> None
# Init the SanicIntegration with the desired arguments
sentry_init(
integrations=[SanicIntegration(*test_config.integration_args)],
traces_sample_rate=1.0,
)
events = capture_events()
# Make request to the desired URL
c = get_client(app)
with c as client:
_, response = client.get(test_config.url)
assert response.status == test_config.expected_status
# Extract the transaction events by inspecting the event types. We should at most have 1 transaction event.
transaction_events = [
e for e in events if "type" in e and e["type"] == "transaction"
]
assert len(transaction_events) <= 1
# Get the only transaction event, or set to None if there are no transaction events.
(transaction_event, *_) = [*transaction_events, None]
# We should have no transaction event if and only if we expect no transactions
assert (transaction_event is None) == (
test_config.expected_transaction_name is None
)
# If a transaction was expected, ensure it is correct
assert (
transaction_event is None
or transaction_event["transaction"] == test_config.expected_transaction_name
)
assert (
transaction_event is None
or transaction_event["transaction_info"]["source"]
== test_config.expected_source
)
@pytest.mark.skipif(
not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version"
)
def test_span_origin(sentry_init, app, capture_events):
sentry_init(integrations=[SanicIntegration()], traces_sample_rate=1.0)
events = capture_events()
c = get_client(app)
with c as client:
client.get("/message?foo=bar")
(_, event) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.sanic"
sentry-python-2.18.0/tests/integrations/serverless/ 0000775 0000000 0000000 00000000000 14712146540 0022516 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/serverless/test_serverless.py 0000664 0000000 0000000 00000001744 14712146540 0026332 0 ustar 00root root 0000000 0000000 import pytest
from sentry_sdk.integrations.serverless import serverless_function
def test_basic(sentry_init, capture_exceptions, monkeypatch):
sentry_init()
exceptions = capture_exceptions()
flush_calls = []
@serverless_function
def foo():
monkeypatch.setattr("sentry_sdk.flush", lambda: flush_calls.append(1))
1 / 0
with pytest.raises(ZeroDivisionError):
foo()
(exception,) = exceptions
assert isinstance(exception, ZeroDivisionError)
assert flush_calls == [1]
def test_flush_disabled(sentry_init, capture_exceptions, monkeypatch):
sentry_init()
exceptions = capture_exceptions()
flush_calls = []
monkeypatch.setattr("sentry_sdk.flush", lambda: flush_calls.append(1))
@serverless_function(flush=False)
def foo():
1 / 0
with pytest.raises(ZeroDivisionError):
foo()
(exception,) = exceptions
assert isinstance(exception, ZeroDivisionError)
assert flush_calls == []
sentry-python-2.18.0/tests/integrations/socket/ 0000775 0000000 0000000 00000000000 14712146540 0021611 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/socket/__init__.py 0000664 0000000 0000000 00000000055 14712146540 0023722 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("socket")
sentry-python-2.18.0/tests/integrations/socket/test_socket.py 0000664 0000000 0000000 00000004311 14712146540 0024511 0 ustar 00root root 0000000 0000000 import socket
from sentry_sdk import start_transaction
from sentry_sdk.integrations.socket import SocketIntegration
from tests.conftest import ApproxDict
def test_getaddrinfo_trace(sentry_init, capture_events):
sentry_init(integrations=[SocketIntegration()], traces_sample_rate=1.0)
events = capture_events()
with start_transaction():
socket.getaddrinfo("example.com", 443)
(event,) = events
(span,) = event["spans"]
assert span["op"] == "socket.dns"
assert span["description"] == "example.com:443"
assert span["data"] == ApproxDict(
{
"host": "example.com",
"port": 443,
}
)
def test_create_connection_trace(sentry_init, capture_events):
timeout = 10
sentry_init(integrations=[SocketIntegration()], traces_sample_rate=1.0)
events = capture_events()
with start_transaction():
socket.create_connection(("example.com", 443), timeout, None)
(event,) = events
(connect_span, dns_span) = event["spans"]
# as getaddrinfo gets called in create_connection it should also contain a dns span
assert connect_span["op"] == "socket.connection"
assert connect_span["description"] == "example.com:443"
assert connect_span["data"] == ApproxDict(
{
"address": ["example.com", 443],
"timeout": timeout,
"source_address": None,
}
)
assert dns_span["op"] == "socket.dns"
assert dns_span["description"] == "example.com:443"
assert dns_span["data"] == ApproxDict(
{
"host": "example.com",
"port": 443,
}
)
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[SocketIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
with start_transaction(name="foo"):
socket.create_connection(("example.com", 443), 1, None)
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["op"] == "socket.connection"
assert event["spans"][0]["origin"] == "auto.socket.socket"
assert event["spans"][1]["op"] == "socket.dns"
assert event["spans"][1]["origin"] == "auto.socket.socket"
sentry-python-2.18.0/tests/integrations/spark/ 0000775 0000000 0000000 00000000000 14712146540 0021441 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/spark/__init__.py 0000664 0000000 0000000 00000000112 14712146540 0023544 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("pyspark")
pytest.importorskip("py4j")
sentry-python-2.18.0/tests/integrations/spark/test_spark.py 0000664 0000000 0000000 00000015701 14712146540 0024176 0 ustar 00root root 0000000 0000000 import pytest
import sys
from unittest.mock import patch
from sentry_sdk.integrations.spark.spark_driver import (
_set_app_properties,
_start_sentry_listener,
SentryListener,
SparkIntegration,
)
from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration
from pyspark import SparkContext
from py4j.protocol import Py4JJavaError
################
# DRIVER TESTS #
################
def test_set_app_properties():
spark_context = SparkContext(appName="Testing123")
_set_app_properties()
assert spark_context.getLocalProperty("sentry_app_name") == "Testing123"
# applicationId generated by sparkContext init
assert (
spark_context.getLocalProperty("sentry_application_id")
== spark_context.applicationId
)
def test_start_sentry_listener():
spark_context = SparkContext.getOrCreate()
gateway = spark_context._gateway
assert gateway._callback_server is None
_start_sentry_listener(spark_context)
assert gateway._callback_server is not None
def test_initialize_spark_integration(sentry_init):
sentry_init(integrations=[SparkIntegration()])
SparkContext.getOrCreate()
@pytest.fixture
def sentry_listener():
listener = SentryListener()
return listener
@pytest.fixture
def mock_add_breadcrumb():
with patch("sentry_sdk.add_breadcrumb") as mock:
yield mock
def test_sentry_listener_on_job_start(sentry_listener, mock_add_breadcrumb):
listener = sentry_listener
class MockJobStart:
def jobId(self): # noqa: N802
return "sample-job-id-start"
mock_job_start = MockJobStart()
listener.onJobStart(mock_job_start)
mock_add_breadcrumb.assert_called_once()
mock_hub = mock_add_breadcrumb.call_args
assert mock_hub.kwargs["level"] == "info"
assert "sample-job-id-start" in mock_hub.kwargs["message"]
@pytest.mark.parametrize(
"job_result, level", [("JobSucceeded", "info"), ("JobFailed", "warning")]
)
def test_sentry_listener_on_job_end(
sentry_listener, mock_add_breadcrumb, job_result, level
):
listener = sentry_listener
class MockJobResult:
def toString(self): # noqa: N802
return job_result
class MockJobEnd:
def jobId(self): # noqa: N802
return "sample-job-id-end"
def jobResult(self): # noqa: N802
result = MockJobResult()
return result
mock_job_end = MockJobEnd()
listener.onJobEnd(mock_job_end)
mock_add_breadcrumb.assert_called_once()
mock_hub = mock_add_breadcrumb.call_args
assert mock_hub.kwargs["level"] == level
assert mock_hub.kwargs["data"]["result"] == job_result
assert "sample-job-id-end" in mock_hub.kwargs["message"]
def test_sentry_listener_on_stage_submitted(sentry_listener, mock_add_breadcrumb):
listener = sentry_listener
class StageInfo:
def stageId(self): # noqa: N802
return "sample-stage-id-submit"
def name(self):
return "run-job"
def attemptId(self): # noqa: N802
return 14
class MockStageSubmitted:
def stageInfo(self): # noqa: N802
stageinf = StageInfo()
return stageinf
mock_stage_submitted = MockStageSubmitted()
listener.onStageSubmitted(mock_stage_submitted)
mock_add_breadcrumb.assert_called_once()
mock_hub = mock_add_breadcrumb.call_args
assert mock_hub.kwargs["level"] == "info"
assert "sample-stage-id-submit" in mock_hub.kwargs["message"]
assert mock_hub.kwargs["data"]["attemptId"] == 14
assert mock_hub.kwargs["data"]["name"] == "run-job"
@pytest.fixture
def get_mock_stage_completed():
def _inner(failure_reason):
class JavaException:
def __init__(self):
self._target_id = "id"
class FailureReason:
def get(self):
if failure_reason:
return "failure-reason"
else:
raise Py4JJavaError("msg", JavaException())
class StageInfo:
def stageId(self): # noqa: N802
return "sample-stage-id-submit"
def name(self):
return "run-job"
def attemptId(self): # noqa: N802
return 14
def failureReason(self): # noqa: N802
return FailureReason()
class MockStageCompleted:
def stageInfo(self): # noqa: N802
return StageInfo()
return MockStageCompleted()
return _inner
def test_sentry_listener_on_stage_completed_success(
sentry_listener, mock_add_breadcrumb, get_mock_stage_completed
):
listener = sentry_listener
mock_stage_completed = get_mock_stage_completed(failure_reason=False)
listener.onStageCompleted(mock_stage_completed)
mock_add_breadcrumb.assert_called_once()
mock_hub = mock_add_breadcrumb.call_args
assert mock_hub.kwargs["level"] == "info"
assert "sample-stage-id-submit" in mock_hub.kwargs["message"]
assert mock_hub.kwargs["data"]["attemptId"] == 14
assert mock_hub.kwargs["data"]["name"] == "run-job"
assert "reason" not in mock_hub.kwargs["data"]
def test_sentry_listener_on_stage_completed_failure(
sentry_listener, mock_add_breadcrumb, get_mock_stage_completed
):
listener = sentry_listener
mock_stage_completed = get_mock_stage_completed(failure_reason=True)
listener.onStageCompleted(mock_stage_completed)
mock_add_breadcrumb.assert_called_once()
mock_hub = mock_add_breadcrumb.call_args
assert mock_hub.kwargs["level"] == "warning"
assert "sample-stage-id-submit" in mock_hub.kwargs["message"]
assert mock_hub.kwargs["data"]["attemptId"] == 14
assert mock_hub.kwargs["data"]["name"] == "run-job"
assert mock_hub.kwargs["data"]["reason"] == "failure-reason"
################
# WORKER TESTS #
################
def test_spark_worker(monkeypatch, sentry_init, capture_events, capture_exceptions):
import pyspark.worker as original_worker
import pyspark.daemon as original_daemon
from pyspark.taskcontext import TaskContext
task_context = TaskContext._getOrCreate()
def mock_main():
task_context._stageId = 0
task_context._attemptNumber = 1
task_context._partitionId = 2
task_context._taskAttemptId = 3
try:
raise ZeroDivisionError
except ZeroDivisionError:
sys.exit(-1)
monkeypatch.setattr(original_worker, "main", mock_main)
sentry_init(integrations=[SparkWorkerIntegration()])
events = capture_events()
exceptions = capture_exceptions()
original_daemon.worker_main()
# SystemExit called, but not recorded as part of event
assert type(exceptions.pop()) == SystemExit
assert len(events[0]["exception"]["values"]) == 1
assert events[0]["exception"]["values"][0]["type"] == "ZeroDivisionError"
assert events[0]["tags"] == {
"stageId": "0",
"attemptNumber": "1",
"partitionId": "2",
"taskAttemptId": "3",
}
sentry-python-2.18.0/tests/integrations/sqlalchemy/ 0000775 0000000 0000000 00000000000 14712146540 0022463 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/sqlalchemy/__init__.py 0000664 0000000 0000000 00000000444 14712146540 0024576 0 ustar 00root root 0000000 0000000 import os
import sys
import pytest
pytest.importorskip("sqlalchemy")
# Load `sqlalchemy_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.18.0/tests/integrations/sqlalchemy/sqlalchemy_helpers/ 0000775 0000000 0000000 00000000000 14712146540 0026347 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/sqlalchemy/sqlalchemy_helpers/__init__.py 0000664 0000000 0000000 00000000000 14712146540 0030446 0 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/sqlalchemy/sqlalchemy_helpers/helpers.py 0000664 0000000 0000000 00000000300 14712146540 0030354 0 ustar 00root root 0000000 0000000 def add_model_to_session(model, session):
session.add(model)
session.commit()
def query_first_model_from_session(model_klass, session):
return session.query(model_klass).first()
sentry-python-2.18.0/tests/integrations/sqlalchemy/test_sqlalchemy.py 0000664 0000000 0000000 00000053454 14712146540 0026251 0 ustar 00root root 0000000 0000000 import os
from datetime import datetime
from unittest import mock
import pytest
from sqlalchemy import Column, ForeignKey, Integer, String, create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
from sqlalchemy import text
import sentry_sdk
from sentry_sdk import capture_message, start_transaction
from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, SPANDATA
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.serializer import MAX_EVENT_BYTES
from sentry_sdk.tracing_utils import record_sql_queries
from sentry_sdk.utils import json_dumps
def test_orm_queries(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()], _experiments={"record_sql_params": True}
)
events = capture_events()
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
street_name = Column(String(250))
street_number = Column(String(250))
post_code = Column(String(250), nullable=False)
person_id = Column(Integer, ForeignKey("person.id"))
person = relationship(Person)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
assert session.query(Person).first() == bob
capture_message("hi")
(event,) = events
for crumb in event["breadcrumbs"]["values"]:
del crumb["timestamp"]
assert event["breadcrumbs"]["values"][-2:] == [
{
"category": "query",
"data": {"db.params": ["Bob"], "db.paramstyle": "qmark"},
"message": "INSERT INTO person (name) VALUES (?)",
"type": "default",
},
{
"category": "query",
"data": {"db.params": [1, 0], "db.paramstyle": "qmark"},
"message": "SELECT person.id AS person_id, person.name AS person_name \n"
"FROM person\n"
" LIMIT ? OFFSET ?",
"type": "default",
},
]
def test_transactions(sentry_init, capture_events, render_span_tree):
sentry_init(
integrations=[SqlalchemyIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
)
events = capture_events()
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
street_name = Column(String(250))
street_number = Column(String(250))
post_code = Column(String(250), nullable=False)
person_id = Column(Integer, ForeignKey("person.id"))
person = relationship(Person)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
with start_transaction(name="test_transaction", sampled=True):
with session.begin_nested():
session.query(Person).first()
for _ in range(2):
with pytest.raises(IntegrityError):
with session.begin_nested():
session.add(Person(id=1, name="bob"))
session.add(Person(id=1, name="bob"))
with session.begin_nested():
session.query(Person).first()
(event,) = events
for span in event["spans"]:
assert span["data"][SPANDATA.DB_SYSTEM] == "sqlite"
assert span["data"][SPANDATA.DB_NAME] == ":memory:"
assert SPANDATA.SERVER_ADDRESS not in span["data"]
assert SPANDATA.SERVER_PORT not in span["data"]
assert (
render_span_tree(event)
== """\
- op=null: description=null
- op="db": description="SAVEPOINT sa_savepoint_1"
- op="db": description="SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?"
- op="db": description="RELEASE SAVEPOINT sa_savepoint_1"
- op="db": description="SAVEPOINT sa_savepoint_2"
- op="db": description="INSERT INTO person (id, name) VALUES (?, ?)"
- op="db": description="ROLLBACK TO SAVEPOINT sa_savepoint_2"
- op="db": description="SAVEPOINT sa_savepoint_3"
- op="db": description="INSERT INTO person (id, name) VALUES (?, ?)"
- op="db": description="ROLLBACK TO SAVEPOINT sa_savepoint_3"
- op="db": description="SAVEPOINT sa_savepoint_4"
- op="db": description="SELECT person.id AS person_id, person.name AS person_name \\nFROM person\\n LIMIT ? OFFSET ?"
- op="db": description="RELEASE SAVEPOINT sa_savepoint_4"\
"""
)
def test_transactions_no_engine_url(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()],
_experiments={"record_sql_params": True},
traces_sample_rate=1.0,
)
events = capture_events()
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
street_name = Column(String(250))
street_number = Column(String(250))
post_code = Column(String(250), nullable=False)
person_id = Column(Integer, ForeignKey("person.id"))
person = relationship(Person)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
engine.url = None
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
with start_transaction(name="test_transaction", sampled=True):
with session.begin_nested():
session.query(Person).first()
for _ in range(2):
with pytest.raises(IntegrityError):
with session.begin_nested():
session.add(Person(id=1, name="bob"))
session.add(Person(id=1, name="bob"))
with session.begin_nested():
session.query(Person).first()
(event,) = events
for span in event["spans"]:
assert span["data"][SPANDATA.DB_SYSTEM] == "sqlite"
assert SPANDATA.DB_NAME not in span["data"]
assert SPANDATA.SERVER_ADDRESS not in span["data"]
assert SPANDATA.SERVER_PORT not in span["data"]
def test_long_sql_query_preserved(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1,
integrations=[SqlalchemyIntegration()],
)
events = capture_events()
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
with start_transaction(name="test"):
with engine.connect() as con:
con.execute(text(" UNION ".join("SELECT {}".format(i) for i in range(100))))
(event,) = events
description = event["spans"][0]["description"]
assert description.startswith("SELECT 0 UNION SELECT 1")
assert description.endswith("SELECT 98 UNION SELECT 99")
def test_large_event_not_truncated(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1,
integrations=[SqlalchemyIntegration()],
)
events = capture_events()
long_str = "x" * (DEFAULT_MAX_VALUE_LENGTH + 10)
scope = sentry_sdk.get_isolation_scope()
@scope.add_event_processor
def processor(event, hint):
event["message"] = long_str
return event
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
with start_transaction(name="test"):
with engine.connect() as con:
for _ in range(1500):
con.execute(
text(" UNION ".join("SELECT {}".format(i) for i in range(100)))
)
(event,) = events
assert len(json_dumps(event)) > MAX_EVENT_BYTES
# Some spans are discarded.
assert len(event["spans"]) == 1000
# Span descriptions are not truncated.
description = event["spans"][0]["description"]
assert len(description) == 1583
assert description.startswith("SELECT 0")
assert description.endswith("SELECT 98 UNION SELECT 99")
description = event["spans"][999]["description"]
assert len(description) == 1583
assert description.startswith("SELECT 0")
assert description.endswith("SELECT 98 UNION SELECT 99")
# Smoke check that truncation of other fields has not changed.
assert len(event["message"]) == DEFAULT_MAX_VALUE_LENGTH
# The _meta for other truncated fields should be there as well.
assert event["_meta"]["message"] == {
"": {"len": 1034, "rem": [["!limit", "x", 1021, 1024]]}
}
def test_engine_name_not_string(sentry_init):
sentry_init(
integrations=[SqlalchemyIntegration()],
)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
engine.dialect.name = b"sqlite"
with engine.connect() as con:
con.execute(text("SELECT 0"))
def test_query_source_disabled(sentry_init, capture_events):
sentry_options = {
"integrations": [SqlalchemyIntegration()],
"enable_tracing": True,
"enable_db_query_source": False,
"db_query_source_threshold_ms": 0,
}
sentry_init(**sentry_options)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
assert session.query(Person).first() == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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.parametrize("enable_db_query_source", [None, True])
def test_query_source_enabled(sentry_init, capture_events, enable_db_query_source):
sentry_options = {
"integrations": [SqlalchemyIntegration()],
"enable_tracing": True,
"db_query_source_threshold_ms": 0,
}
if enable_db_query_source is not None:
sentry_options["enable_db_query_source"] = enable_db_query_source
sentry_init(**sentry_options)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
assert session.query(Person).first() == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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")
def test_query_source(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
assert session.query(Person).first() == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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.sqlalchemy.test_sqlalchemy"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/sqlalchemy/test_sqlalchemy.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"
break
else:
raise AssertionError("No db span found")
def test_query_source_with_module_in_search_path(sentry_init, capture_events):
"""
Test that query source is relative to the path of the module it ran in
"""
sentry_init(
integrations=[SqlalchemyIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=0,
)
events = capture_events()
from sqlalchemy_helpers.helpers import (
add_model_to_session,
query_first_model_from_session,
)
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
add_model_to_session(bob, session)
assert query_first_model_from_session(Person, session) == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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) == "sqlalchemy_helpers.helpers"
assert data.get(SPANDATA.CODE_FILEPATH) == "sqlalchemy_helpers/helpers.py"
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
assert is_relative_path
assert data.get(SPANDATA.CODE_FUNCTION) == "query_first_model_from_session"
break
else:
raise AssertionError("No db span found")
def test_no_query_source_if_duration_too_short(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
self.span = span
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=99999)
def __enter__(self):
return self.span
def __exit__(self, type, value, traceback):
pass
with mock.patch(
"sentry_sdk.integrations.sqlalchemy.record_sql_queries",
fake_record_sql_queries,
):
assert session.query(Person).first() == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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")
def test_query_source_if_duration_over_threshold(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()],
enable_tracing=True,
enable_db_query_source=True,
db_query_source_threshold_ms=100,
)
events = capture_events()
with start_transaction(name="test_transaction", sampled=True):
Base = declarative_base() # noqa: N806
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String(250), nullable=False)
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine) # noqa: N806
session = Session()
bob = Person(name="Bob")
session.add(bob)
class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
with record_sql_queries(*args, **kwargs) as span:
self.span = span
self.span.start_timestamp = datetime(2024, 1, 1, microsecond=0)
self.span.timestamp = datetime(2024, 1, 1, microsecond=101000)
def __enter__(self):
return self.span
def __exit__(self, type, value, traceback):
pass
with mock.patch(
"sentry_sdk.integrations.sqlalchemy.record_sql_queries",
fake_record_sql_queries,
):
assert session.query(Person).first() == bob
(event,) = events
for span in event["spans"]:
if span.get("op") == "db" and span.get("description").startswith(
"SELECT person"
):
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.sqlalchemy.test_sqlalchemy"
)
assert data.get(SPANDATA.CODE_FILEPATH).endswith(
"tests/integrations/sqlalchemy/test_sqlalchemy.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"
)
break
else:
raise AssertionError("No db span found")
def test_span_origin(sentry_init, capture_events):
sentry_init(
integrations=[SqlalchemyIntegration()],
traces_sample_rate=1.0,
)
events = capture_events()
engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}
)
with start_transaction(name="foo"):
with engine.connect() as con:
con.execute(text("SELECT 0"))
(event,) = events
assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.db.sqlalchemy"
sentry-python-2.18.0/tests/integrations/starlette/ 0000775 0000000 0000000 00000000000 14712146540 0022330 5 ustar 00root root 0000000 0000000 sentry-python-2.18.0/tests/integrations/starlette/__init__.py 0000664 0000000 0000000 00000000060 14712146540 0024435 0 ustar 00root root 0000000 0000000 import pytest
pytest.importorskip("starlette")
sentry-python-2.18.0/tests/integrations/starlette/photo.jpg 0000664 0000000 0000000 00000051026 14712146540 0024167 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