about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis')
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py38
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py108
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py113
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py19
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py121
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py68
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py32
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py69
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py99
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py50
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py144
12 files changed, 861 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py
new file mode 100644
index 00000000..f4431382
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py
@@ -0,0 +1,38 @@
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE
+from sentry_sdk.integrations.redis.rb import _patch_rb
+from sentry_sdk.integrations.redis.redis import _patch_redis
+from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster
+from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster
+from sentry_sdk.utils import logger
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Optional
+
+
+class RedisIntegration(Integration):
+    identifier = "redis"
+
+    def __init__(self, max_data_size=_DEFAULT_MAX_DATA_SIZE, cache_prefixes=None):
+        # type: (int, Optional[list[str]]) -> None
+        self.max_data_size = max_data_size
+        self.cache_prefixes = cache_prefixes if cache_prefixes is not None else []
+
+    @staticmethod
+    def setup_once():
+        # type: () -> None
+        try:
+            from redis import StrictRedis, client
+        except ImportError:
+            raise DidNotEnable("Redis client not installed")
+
+        _patch_redis(StrictRedis, client)
+        _patch_redis_cluster()
+        _patch_rb()
+
+        try:
+            _patch_rediscluster()
+        except Exception:
+            logger.exception("Error occurred while patching `rediscluster` library")
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py
new file mode 100644
index 00000000..196e85e7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py
@@ -0,0 +1,108 @@
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN
+from sentry_sdk.integrations.redis.modules.caches import (
+    _compile_cache_span_properties,
+    _set_cache_data,
+)
+from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
+from sentry_sdk.integrations.redis.utils import (
+    _set_client_data,
+    _set_pipeline_data,
+)
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from collections.abc import Callable
+    from typing import Any, Union
+    from redis.asyncio.client import Pipeline, StrictRedis
+    from redis.asyncio.cluster import ClusterPipeline, RedisCluster
+
+
+def patch_redis_async_pipeline(
+    pipeline_cls, is_cluster, get_command_args_fn, set_db_data_fn
+):
+    # type: (Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]], bool, Any, Callable[[Span, Any], None]) -> None
+    old_execute = pipeline_cls.execute
+
+    from sentry_sdk.integrations.redis import RedisIntegration
+
+    async def _sentry_execute(self, *args, **kwargs):
+        # type: (Any, *Any, **Any) -> Any
+        if sentry_sdk.get_client().get_integration(RedisIntegration) is None:
+            return await old_execute(self, *args, **kwargs)
+
+        with sentry_sdk.start_span(
+            op=OP.DB_REDIS,
+            name="redis.pipeline.execute",
+            origin=SPAN_ORIGIN,
+        ) as span:
+            with capture_internal_exceptions():
+                set_db_data_fn(span, self)
+                _set_pipeline_data(
+                    span,
+                    is_cluster,
+                    get_command_args_fn,
+                    False if is_cluster else self.is_transaction,
+                    self._command_stack if is_cluster else self.command_stack,
+                )
+
+            return await old_execute(self, *args, **kwargs)
+
+    pipeline_cls.execute = _sentry_execute  # type: ignore
+
+
+def patch_redis_async_client(cls, is_cluster, set_db_data_fn):
+    # type: (Union[type[StrictRedis[Any]], type[RedisCluster[Any]]], bool, Callable[[Span, Any], None]) -> None
+    old_execute_command = cls.execute_command
+
+    from sentry_sdk.integrations.redis import RedisIntegration
+
+    async def _sentry_execute_command(self, name, *args, **kwargs):
+        # type: (Any, str, *Any, **Any) -> Any
+        integration = sentry_sdk.get_client().get_integration(RedisIntegration)
+        if integration is None:
+            return await old_execute_command(self, name, *args, **kwargs)
+
+        cache_properties = _compile_cache_span_properties(
+            name,
+            args,
+            kwargs,
+            integration,
+        )
+
+        cache_span = None
+        if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
+            cache_span = sentry_sdk.start_span(
+                op=cache_properties["op"],
+                name=cache_properties["description"],
+                origin=SPAN_ORIGIN,
+            )
+            cache_span.__enter__()
+
+        db_properties = _compile_db_span_properties(integration, name, args)
+
+        db_span = sentry_sdk.start_span(
+            op=db_properties["op"],
+            name=db_properties["description"],
+            origin=SPAN_ORIGIN,
+        )
+        db_span.__enter__()
+
+        set_db_data_fn(db_span, self)
+        _set_client_data(db_span, is_cluster, name, *args)
+
+        value = await old_execute_command(self, name, *args, **kwargs)
+
+        db_span.__exit__(None, None, None)
+
+        if cache_span:
+            _set_cache_data(cache_span, self, cache_properties, value)
+            cache_span.__exit__(None, None, None)
+
+        return value
+
+    cls.execute_command = _sentry_execute_command  # type: ignore
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py
new file mode 100644
index 00000000..ef10e9e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py
@@ -0,0 +1,113 @@
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN
+from sentry_sdk.integrations.redis.modules.caches import (
+    _compile_cache_span_properties,
+    _set_cache_data,
+)
+from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
+from sentry_sdk.integrations.redis.utils import (
+    _set_client_data,
+    _set_pipeline_data,
+)
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from collections.abc import Callable
+    from typing import Any
+
+
+def patch_redis_pipeline(
+    pipeline_cls,
+    is_cluster,
+    get_command_args_fn,
+    set_db_data_fn,
+):
+    # type: (Any, bool, Any, Callable[[Span, Any], None]) -> None
+    old_execute = pipeline_cls.execute
+
+    from sentry_sdk.integrations.redis import RedisIntegration
+
+    def sentry_patched_execute(self, *args, **kwargs):
+        # type: (Any, *Any, **Any) -> Any
+        if sentry_sdk.get_client().get_integration(RedisIntegration) is None:
+            return old_execute(self, *args, **kwargs)
+
+        with sentry_sdk.start_span(
+            op=OP.DB_REDIS,
+            name="redis.pipeline.execute",
+            origin=SPAN_ORIGIN,
+        ) as span:
+            with capture_internal_exceptions():
+                set_db_data_fn(span, self)
+                _set_pipeline_data(
+                    span,
+                    is_cluster,
+                    get_command_args_fn,
+                    False if is_cluster else self.transaction,
+                    self.command_stack,
+                )
+
+            return old_execute(self, *args, **kwargs)
+
+    pipeline_cls.execute = sentry_patched_execute
+
+
+def patch_redis_client(cls, is_cluster, set_db_data_fn):
+    # type: (Any, bool, Callable[[Span, Any], None]) -> None
+    """
+    This function can be used to instrument custom redis client classes or
+    subclasses.
+    """
+    old_execute_command = cls.execute_command
+
+    from sentry_sdk.integrations.redis import RedisIntegration
+
+    def sentry_patched_execute_command(self, name, *args, **kwargs):
+        # type: (Any, str, *Any, **Any) -> Any
+        integration = sentry_sdk.get_client().get_integration(RedisIntegration)
+        if integration is None:
+            return old_execute_command(self, name, *args, **kwargs)
+
+        cache_properties = _compile_cache_span_properties(
+            name,
+            args,
+            kwargs,
+            integration,
+        )
+
+        cache_span = None
+        if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
+            cache_span = sentry_sdk.start_span(
+                op=cache_properties["op"],
+                name=cache_properties["description"],
+                origin=SPAN_ORIGIN,
+            )
+            cache_span.__enter__()
+
+        db_properties = _compile_db_span_properties(integration, name, args)
+
+        db_span = sentry_sdk.start_span(
+            op=db_properties["op"],
+            name=db_properties["description"],
+            origin=SPAN_ORIGIN,
+        )
+        db_span.__enter__()
+
+        set_db_data_fn(db_span, self)
+        _set_client_data(db_span, is_cluster, name, *args)
+
+        value = old_execute_command(self, name, *args, **kwargs)
+
+        db_span.__exit__(None, None, None)
+
+        if cache_span:
+            _set_cache_data(cache_span, self, cache_properties, value)
+            cache_span.__exit__(None, None, None)
+
+        return value
+
+    cls.execute_command = sentry_patched_execute_command
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py
new file mode 100644
index 00000000..737e8297
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py
@@ -0,0 +1,19 @@
+SPAN_ORIGIN = "auto.db.redis"
+
+_SINGLE_KEY_COMMANDS = frozenset(
+    ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"],
+)
+_MULTI_KEY_COMMANDS = frozenset(
+    [
+        "del",
+        "touch",
+        "unlink",
+        "mget",
+    ],
+)
+_COMMANDS_INCLUDING_SENSITIVE_DATA = [
+    "auth",
+]
+_MAX_NUM_ARGS = 10  # Trim argument lists to this many values
+_MAX_NUM_COMMANDS = 10  # Trim command lists to this many values
+_DEFAULT_MAX_DATA_SIZE = 1024
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py
new file mode 100644
index 00000000..c6fc19f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py
@@ -0,0 +1,121 @@
+"""
+Code used for the Caches module in Sentry
+"""
+
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
+from sentry_sdk.utils import capture_internal_exceptions
+
+GET_COMMANDS = ("get", "mget")
+SET_COMMANDS = ("set", "setex")
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from sentry_sdk.integrations.redis import RedisIntegration
+    from sentry_sdk.tracing import Span
+    from typing import Any, Optional
+
+
+def _get_op(name):
+    # type: (str) -> Optional[str]
+    op = None
+    if name.lower() in GET_COMMANDS:
+        op = OP.CACHE_GET
+    elif name.lower() in SET_COMMANDS:
+        op = OP.CACHE_PUT
+
+    return op
+
+
+def _compile_cache_span_properties(redis_command, args, kwargs, integration):
+    # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> dict[str, Any]
+    key = _get_safe_key(redis_command, args, kwargs)
+    key_as_string = _key_as_string(key)
+    keys_as_string = key_as_string.split(", ")
+
+    is_cache_key = False
+    for prefix in integration.cache_prefixes:
+        for kee in keys_as_string:
+            if kee.startswith(prefix):
+                is_cache_key = True
+                break
+        if is_cache_key:
+            break
+
+    value = None
+    if redis_command.lower() in SET_COMMANDS:
+        value = args[-1]
+
+    properties = {
+        "op": _get_op(redis_command),
+        "description": _get_cache_span_description(
+            redis_command, args, kwargs, integration
+        ),
+        "key": key,
+        "key_as_string": key_as_string,
+        "redis_command": redis_command.lower(),
+        "is_cache_key": is_cache_key,
+        "value": value,
+    }
+
+    return properties
+
+
+def _get_cache_span_description(redis_command, args, kwargs, integration):
+    # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> str
+    description = _key_as_string(_get_safe_key(redis_command, args, kwargs))
+
+    data_should_be_truncated = (
+        integration.max_data_size and len(description) > integration.max_data_size
+    )
+    if data_should_be_truncated:
+        description = description[: integration.max_data_size - len("...")] + "..."
+
+    return description
+
+
+def _set_cache_data(span, redis_client, properties, return_value):
+    # type: (Span, Any, dict[str, Any], Optional[Any]) -> None
+    with capture_internal_exceptions():
+        span.set_data(SPANDATA.CACHE_KEY, properties["key"])
+
+        if properties["redis_command"] in GET_COMMANDS:
+            if return_value is not None:
+                span.set_data(SPANDATA.CACHE_HIT, True)
+                size = (
+                    len(str(return_value).encode("utf-8"))
+                    if not isinstance(return_value, bytes)
+                    else len(return_value)
+                )
+                span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
+            else:
+                span.set_data(SPANDATA.CACHE_HIT, False)
+
+        elif properties["redis_command"] in SET_COMMANDS:
+            if properties["value"] is not None:
+                size = (
+                    len(properties["value"].encode("utf-8"))
+                    if not isinstance(properties["value"], bytes)
+                    else len(properties["value"])
+                )
+                span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
+
+        try:
+            connection_params = redis_client.connection_pool.connection_kwargs
+        except AttributeError:
+            # If it is a cluster, there is no connection_pool attribute so we
+            # need to get the default node from the cluster instance
+            default_node = redis_client.get_default_node()
+            connection_params = {
+                "host": default_node.host,
+                "port": default_node.port,
+            }
+
+        host = connection_params.get("host")
+        if host is not None:
+            span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, host)
+
+        port = connection_params.get("port")
+        if port is not None:
+            span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py
new file mode 100644
index 00000000..e0d85a4e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py
@@ -0,0 +1,68 @@
+"""
+Code used for the Queries module in Sentry
+"""
+
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.integrations.redis.utils import _get_safe_command
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from redis import Redis
+    from sentry_sdk.integrations.redis import RedisIntegration
+    from sentry_sdk.tracing import Span
+    from typing import Any
+
+
+def _compile_db_span_properties(integration, redis_command, args):
+    # type: (RedisIntegration, str, tuple[Any, ...]) -> dict[str, Any]
+    description = _get_db_span_description(integration, redis_command, args)
+
+    properties = {
+        "op": OP.DB_REDIS,
+        "description": description,
+    }
+
+    return properties
+
+
+def _get_db_span_description(integration, command_name, args):
+    # type: (RedisIntegration, str, tuple[Any, ...]) -> str
+    description = command_name
+
+    with capture_internal_exceptions():
+        description = _get_safe_command(command_name, args)
+
+    data_should_be_truncated = (
+        integration.max_data_size and len(description) > integration.max_data_size
+    )
+    if data_should_be_truncated:
+        description = description[: integration.max_data_size - len("...")] + "..."
+
+    return description
+
+
+def _set_db_data_on_span(span, connection_params):
+    # type: (Span, dict[str, Any]) -> None
+    span.set_data(SPANDATA.DB_SYSTEM, "redis")
+
+    db = connection_params.get("db")
+    if db is not None:
+        span.set_data(SPANDATA.DB_NAME, str(db))
+
+    host = connection_params.get("host")
+    if host is not None:
+        span.set_data(SPANDATA.SERVER_ADDRESS, host)
+
+    port = connection_params.get("port")
+    if port is not None:
+        span.set_data(SPANDATA.SERVER_PORT, port)
+
+
+def _set_db_data(span, redis_instance):
+    # type: (Span, Redis[Any]) -> None
+    try:
+        _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs)
+    except AttributeError:
+        pass  # connections_kwargs may be missing in some cases
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py
new file mode 100644
index 00000000..1b3e2e53
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py
@@ -0,0 +1,32 @@
+"""
+Instrumentation for Redis Blaster (rb)
+
+https://github.com/getsentry/rb
+"""
+
+from sentry_sdk.integrations.redis._sync_common import patch_redis_client
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+
+
+def _patch_rb():
+    # type: () -> None
+    try:
+        import rb.clients  # type: ignore
+    except ImportError:
+        pass
+    else:
+        patch_redis_client(
+            rb.clients.FanoutClient,
+            is_cluster=False,
+            set_db_data_fn=_set_db_data,
+        )
+        patch_redis_client(
+            rb.clients.MappingClient,
+            is_cluster=False,
+            set_db_data_fn=_set_db_data,
+        )
+        patch_redis_client(
+            rb.clients.RoutingClient,
+            is_cluster=False,
+            set_db_data_fn=_set_db_data,
+        )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py
new file mode 100644
index 00000000..c92958a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py
@@ -0,0 +1,69 @@
+"""
+Instrumentation for Redis
+
+https://github.com/redis/redis-py
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+    patch_redis_client,
+    patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any, Sequence
+
+
+def _get_redis_command_args(command):
+    # type: (Any) -> Sequence[Any]
+    return command[0]
+
+
+def _patch_redis(StrictRedis, client):  # noqa: N803
+    # type: (Any, Any) -> None
+    patch_redis_client(
+        StrictRedis,
+        is_cluster=False,
+        set_db_data_fn=_set_db_data,
+    )
+    patch_redis_pipeline(
+        client.Pipeline,
+        is_cluster=False,
+        get_command_args_fn=_get_redis_command_args,
+        set_db_data_fn=_set_db_data,
+    )
+    try:
+        strict_pipeline = client.StrictPipeline
+    except AttributeError:
+        pass
+    else:
+        patch_redis_pipeline(
+            strict_pipeline,
+            is_cluster=False,
+            get_command_args_fn=_get_redis_command_args,
+            set_db_data_fn=_set_db_data,
+        )
+
+    try:
+        import redis.asyncio
+    except ImportError:
+        pass
+    else:
+        from sentry_sdk.integrations.redis._async_common import (
+            patch_redis_async_client,
+            patch_redis_async_pipeline,
+        )
+
+        patch_redis_async_client(
+            redis.asyncio.client.StrictRedis,
+            is_cluster=False,
+            set_db_data_fn=_set_db_data,
+        )
+        patch_redis_async_pipeline(
+            redis.asyncio.client.Pipeline,
+            False,
+            _get_redis_command_args,
+            set_db_data_fn=_set_db_data,
+        )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py
new file mode 100644
index 00000000..80cdc723
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py
@@ -0,0 +1,99 @@
+"""
+Instrumentation for RedisCluster
+This is part of the main redis-py client.
+
+https://github.com/redis/redis-py/blob/master/redis/cluster.py
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+    patch_redis_client,
+    patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span
+from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
+
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+    from redis import RedisCluster
+    from redis.asyncio.cluster import (
+        RedisCluster as AsyncRedisCluster,
+        ClusterPipeline as AsyncClusterPipeline,
+    )
+    from sentry_sdk.tracing import Span
+
+
+def _set_async_cluster_db_data(span, async_redis_cluster_instance):
+    # type: (Span, AsyncRedisCluster[Any]) -> None
+    default_node = async_redis_cluster_instance.get_default_node()
+    if default_node is not None and default_node.connection_kwargs is not None:
+        _set_db_data_on_span(span, default_node.connection_kwargs)
+
+
+def _set_async_cluster_pipeline_db_data(span, async_redis_cluster_pipeline_instance):
+    # type: (Span, AsyncClusterPipeline[Any]) -> None
+    with capture_internal_exceptions():
+        _set_async_cluster_db_data(
+            span,
+            # the AsyncClusterPipeline has always had a `_client` attr but it is private so potentially problematic and mypy
+            # does not recognize it - see https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
+            async_redis_cluster_pipeline_instance._client,  # type: ignore[attr-defined]
+        )
+
+
+def _set_cluster_db_data(span, redis_cluster_instance):
+    # type: (Span, RedisCluster[Any]) -> None
+    default_node = redis_cluster_instance.get_default_node()
+
+    if default_node is not None:
+        connection_params = {
+            "host": default_node.host,
+            "port": default_node.port,
+        }
+        _set_db_data_on_span(span, connection_params)
+
+
+def _patch_redis_cluster():
+    # type: () -> None
+    """Patches the cluster module on redis SDK (as opposed to rediscluster library)"""
+    try:
+        from redis import RedisCluster, cluster
+    except ImportError:
+        pass
+    else:
+        patch_redis_client(
+            RedisCluster,
+            is_cluster=True,
+            set_db_data_fn=_set_cluster_db_data,
+        )
+        patch_redis_pipeline(
+            cluster.ClusterPipeline,
+            is_cluster=True,
+            get_command_args_fn=_parse_rediscluster_command,
+            set_db_data_fn=_set_cluster_db_data,
+        )
+
+    try:
+        from redis.asyncio import cluster as async_cluster
+    except ImportError:
+        pass
+    else:
+        from sentry_sdk.integrations.redis._async_common import (
+            patch_redis_async_client,
+            patch_redis_async_pipeline,
+        )
+
+        patch_redis_async_client(
+            async_cluster.RedisCluster,
+            is_cluster=True,
+            set_db_data_fn=_set_async_cluster_db_data,
+        )
+        patch_redis_async_pipeline(
+            async_cluster.ClusterPipeline,
+            is_cluster=True,
+            get_command_args_fn=_parse_rediscluster_command,
+            set_db_data_fn=_set_async_cluster_pipeline_db_data,
+        )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py
new file mode 100644
index 00000000..ad1c2363
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py
@@ -0,0 +1,50 @@
+"""
+Instrumentation for redis-py-cluster
+The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021).
+
+https://github.com/grokzen/redis-py-cluster
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+    patch_redis_client,
+    patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
+
+
+def _patch_rediscluster():
+    # type: () -> None
+    try:
+        import rediscluster  # type: ignore
+    except ImportError:
+        return
+
+    patch_redis_client(
+        rediscluster.RedisCluster,
+        is_cluster=True,
+        set_db_data_fn=_set_db_data,
+    )
+
+    # up to v1.3.6, __version__ attribute is a tuple
+    # from v2.0.0, __version__ is a string and VERSION a tuple
+    version = getattr(rediscluster, "VERSION", rediscluster.__version__)
+
+    # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0
+    # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst
+    if (0, 2, 0) < version < (2, 0, 0):
+        pipeline_cls = rediscluster.pipeline.StrictClusterPipeline
+        patch_redis_client(
+            rediscluster.StrictRedisCluster,
+            is_cluster=True,
+            set_db_data_fn=_set_db_data,
+        )
+    else:
+        pipeline_cls = rediscluster.pipeline.ClusterPipeline
+
+    patch_redis_pipeline(
+        pipeline_cls,
+        is_cluster=True,
+        get_command_args_fn=_parse_rediscluster_command,
+        set_db_data_fn=_set_db_data,
+    )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py
new file mode 100644
index 00000000..27fae1e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py
@@ -0,0 +1,144 @@
+from sentry_sdk.consts import SPANDATA
+from sentry_sdk.integrations.redis.consts import (
+    _COMMANDS_INCLUDING_SENSITIVE_DATA,
+    _MAX_NUM_ARGS,
+    _MAX_NUM_COMMANDS,
+    _MULTI_KEY_COMMANDS,
+    _SINGLE_KEY_COMMANDS,
+)
+from sentry_sdk.scope import should_send_default_pii
+from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any, Optional, Sequence
+    from sentry_sdk.tracing import Span
+
+
+def _get_safe_command(name, args):
+    # type: (str, Sequence[Any]) -> str
+    command_parts = [name]
+
+    for i, arg in enumerate(args):
+        if i > _MAX_NUM_ARGS:
+            break
+
+        name_low = name.lower()
+
+        if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA:
+            command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
+            continue
+
+        arg_is_the_key = i == 0
+        if arg_is_the_key:
+            command_parts.append(repr(arg))
+
+        else:
+            if should_send_default_pii():
+                command_parts.append(repr(arg))
+            else:
+                command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
+
+    command = " ".join(command_parts)
+    return command
+
+
+def _safe_decode(key):
+    # type: (Any) -> str
+    if isinstance(key, bytes):
+        try:
+            return key.decode()
+        except UnicodeDecodeError:
+            return ""
+
+    return str(key)
+
+
+def _key_as_string(key):
+    # type: (Any) -> str
+    if isinstance(key, (dict, list, tuple)):
+        key = ", ".join(_safe_decode(x) for x in key)
+    elif isinstance(key, bytes):
+        key = _safe_decode(key)
+    elif key is None:
+        key = ""
+    else:
+        key = str(key)
+
+    return key
+
+
+def _get_safe_key(method_name, args, kwargs):
+    # type: (str, Optional[tuple[Any, ...]], Optional[dict[str, Any]]) -> Optional[tuple[str, ...]]
+    """
+    Gets the key (or keys) from the given method_name.
+    The method_name could be a redis command or a django caching command
+    """
+    key = None
+
+    if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS:
+        # for example redis "mget"
+        key = tuple(args)
+
+    elif args is not None and len(args) >= 1:
+        # for example django "set_many/get_many" or redis "get"
+        if isinstance(args[0], (dict, list, tuple)):
+            key = tuple(args[0])
+        else:
+            key = (args[0],)
+
+    elif kwargs is not None and "key" in kwargs:
+        # this is a legacy case for older versions of Django
+        if isinstance(kwargs["key"], (list, tuple)):
+            if len(kwargs["key"]) > 0:
+                key = tuple(kwargs["key"])
+        else:
+            if kwargs["key"] is not None:
+                key = (kwargs["key"],)
+
+    return key
+
+
+def _parse_rediscluster_command(command):
+    # type: (Any) -> Sequence[Any]
+    return command.args
+
+
+def _set_pipeline_data(
+    span, is_cluster, get_command_args_fn, is_transaction, command_stack
+):
+    # type: (Span, bool, Any, bool, Sequence[Any]) -> None
+    span.set_tag("redis.is_cluster", is_cluster)
+    span.set_tag("redis.transaction", is_transaction)
+
+    commands = []
+    for i, arg in enumerate(command_stack):
+        if i >= _MAX_NUM_COMMANDS:
+            break
+
+        command = get_command_args_fn(arg)
+        commands.append(_get_safe_command(command[0], command[1:]))
+
+    span.set_data(
+        "redis.commands",
+        {
+            "count": len(command_stack),
+            "first_ten": commands,
+        },
+    )
+
+
+def _set_client_data(span, is_cluster, name, *args):
+    # type: (Span, bool, str, *Any) -> None
+    span.set_tag("redis.is_cluster", is_cluster)
+    if name:
+        span.set_tag("redis.command", name)
+        span.set_tag(SPANDATA.DB_OPERATION, name)
+
+    if name and args:
+        name_low = name.lower()
+        if (name_low in _SINGLE_KEY_COMMANDS) or (
+            name_low in _MULTI_KEY_COMMANDS and len(args) == 1
+        ):
+            span.set_tag("redis.key", args[0])