Skip to content

tux.utils.tracing

Sentry Instrumentation Utilities for Tracing and Performance Monitoring.

This module provides a set of decorators and context managers to simplify the instrumentation of code with Sentry transactions and spans. It standardizes the creation of performance monitoring traces and ensures that they gracefully handle cases where the Sentry SDK is not initialized by providing dummy objects.

The main components are: - Decorators (@transaction, @span): For easily wrapping entire functions or methods in a Sentry transaction or span. - Context Managers (start_transaction, start_span): For instrumenting specific blocks of code within a function. - Helper Functions: For adding contextual data to the currently active span.

Classes:

Name Description
DummySpan

A no-op (dummy) span object for when the Sentry SDK is not initialized.

DummyTransaction

A no-op (dummy) transaction object for when Sentry is not initialized.

Functions:

Name Description
safe_set_name

Safely set the name on a span or transaction object.

create_instrumentation_wrapper

Creates an instrumentation wrapper for both sync and async functions.

transaction

Decorator to wrap a function with a Sentry transaction.

span

Decorator to wrap a function with a Sentry span.

start_span

Context manager for creating a Sentry span for a block of code.

start_transaction

Context manager for creating a Sentry transaction for a block of code.

add_tag_to_current_span

Add a tag to the current active Sentry span, if it exists.

add_data_to_current_span

Add data to the current active Sentry span, if it exists.

set_span_attributes

Set multiple tags and data attributes on the current active Sentry span.

set_span_status

Set status on the current span.

set_setup_phase_tag

Set a setup phase tag on the span.

set_span_error

Set error information on a span with consistent patterns.

capture_span_exception

Capture an exception in the current span with consistent error handling.

enhanced_span

Enhanced context manager for creating a Sentry span with initial data.

instrument_bot_commands

Automatically instruments all bot commands with Sentry transactions.

Classes

DummySpan()

A no-op (dummy) span object for when the Sentry SDK is not initialized.

This class mimics the interface of a Sentry span but performs no actions, allowing instrumentation code (with start_span(...)) to run without errors even if Sentry is disabled.

Initialize the dummy span.

Methods:

Name Description
set_tag

No-op tag setter.

set_data

No-op data setter.

set_status

No-op status setter.

set_name

No-op name setter.

Source code in tux/utils/tracing.py
Python
def __init__(self) -> None:
    """Initialize the dummy span."""
    self.start_time = time.perf_counter()

Functions

set_tag(*args: Any, **kwargs: Any) -> DummySpan

No-op tag setter.

Source code in tux/utils/tracing.py
Python
def set_tag(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op tag setter."""
    return self
set_data(*args: Any, **kwargs: Any) -> DummySpan

No-op data setter.

Source code in tux/utils/tracing.py
Python
def set_data(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op data setter."""
    return self
set_status(*args: Any, **kwargs: Any) -> DummySpan

No-op status setter.

Source code in tux/utils/tracing.py
Python
def set_status(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op status setter."""
    return self
set_name(name: str) -> DummySpan

No-op name setter.

Source code in tux/utils/tracing.py
Python
def set_name(self, name: str) -> "DummySpan":
    """No-op name setter."""
    return self

DummyTransaction()

Bases: DummySpan

A no-op (dummy) transaction object for when Sentry is not initialized.

This inherits from DummySpan and provides a safe fallback for the start_transaction context manager.

Initialize the dummy span.

Methods:

Name Description
set_tag

No-op tag setter.

set_data

No-op data setter.

set_status

No-op status setter.

set_name

No-op name setter.

Source code in tux/utils/tracing.py
Python
def __init__(self) -> None:
    """Initialize the dummy span."""
    self.start_time = time.perf_counter()

Functions

set_tag(*args: Any, **kwargs: Any) -> DummySpan

No-op tag setter.

Source code in tux/utils/tracing.py
Python
def set_tag(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op tag setter."""
    return self
set_data(*args: Any, **kwargs: Any) -> DummySpan

No-op data setter.

Source code in tux/utils/tracing.py
Python
def set_data(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op data setter."""
    return self
set_status(*args: Any, **kwargs: Any) -> DummySpan

No-op status setter.

Source code in tux/utils/tracing.py
Python
def set_status(self, *args: Any, **kwargs: Any) -> "DummySpan":
    """No-op status setter."""
    return self
set_name(name: str) -> DummySpan

No-op name setter.

Source code in tux/utils/tracing.py
Python
def set_name(self, name: str) -> "DummySpan":
    """No-op name setter."""
    return self

Functions

safe_set_name(obj: Any, name: str) -> None

Safely set the name on a span or transaction object.

This helper is used because the set_name method may not always be present on all span-like objects from Sentry, so this avoids potential AttributeError exceptions.

Parameters:

Name Type Description Default
obj Any

The span or transaction object.

required
name str

The name to set.

required
Source code in tux/utils/tracing.py
Python
def safe_set_name(obj: Any, name: str) -> None:
    """
    Safely set the name on a span or transaction object.

    This helper is used because the `set_name` method may not always be
    present on all span-like objects from Sentry, so this avoids
    potential `AttributeError` exceptions.

    Parameters
    ----------
    obj : Any
        The span or transaction object.
    name : str
        The name to set.
    """
    if hasattr(obj, "set_name"):
        # Use getattr to avoid static type checking issues
        set_name_func = obj.set_name
        set_name_func(name)

_handle_exception_in_sentry_context(context_obj: Any, exception: Exception) -> None

Handle exceptions in a Sentry context (span or transaction) with consistent patterns.

Parameters:

Name Type Description Default
context_obj Any

The Sentry span or transaction object.

required
exception Exception

The exception that occurred.

required
Source code in tux/utils/tracing.py
Python
def _handle_exception_in_sentry_context(context_obj: Any, exception: Exception) -> None:
    """
    Handle exceptions in a Sentry context (span or transaction) with consistent patterns.

    Parameters
    ----------
    context_obj : Any
        The Sentry span or transaction object.
    exception : Exception
        The exception that occurred.
    """
    context_obj.set_status("internal_error")
    context_obj.set_data("error", str(exception))
    context_obj.set_data("traceback", traceback.format_exc())

_finalize_sentry_context(context_obj: Any, start_time: float) -> None

Finalize a Sentry context with timing information.

Parameters:

Name Type Description Default
context_obj Any

The Sentry span or transaction object.

required
start_time float

The start time for duration calculation.

required
Source code in tux/utils/tracing.py
Python
def _finalize_sentry_context(context_obj: Any, start_time: float) -> None:
    """
    Finalize a Sentry context with timing information.

    Parameters
    ----------
    context_obj : Any
        The Sentry span or transaction object.
    start_time : float
        The start time for duration calculation.
    """
    context_obj.set_data("duration_ms", (time.perf_counter() - start_time) * 1000)

create_instrumentation_wrapper(func: Callable[P, R], context_factory: Callable[[], Any], is_transaction: bool = False) -> Callable[P, R]

Creates an instrumentation wrapper for both sync and async functions.

This is the core helper that eliminates duplication between transaction and span decorators by providing a unified wrapper creation mechanism.

Parameters:

Name Type Description Default
func Callable[P, R]

The function to wrap.

required
context_factory Callable[[], Any]

A factory function that creates the Sentry context (span or transaction).

required
is_transaction bool

Whether this is a transaction (affects status setting behavior).

False

Returns:

Type Description
Callable[P, R]

The wrapped function.

Source code in tux/utils/tracing.py
Python
def create_instrumentation_wrapper[**P, R](
    func: Callable[P, R],
    context_factory: Callable[[], Any],
    is_transaction: bool = False,
) -> Callable[P, R]:
    """
    Creates an instrumentation wrapper for both sync and async functions.

    This is the core helper that eliminates duplication between transaction
    and span decorators by providing a unified wrapper creation mechanism.

    Parameters
    ----------
    func : Callable[P, R]
        The function to wrap.
    context_factory : Callable[[], Any]
        A factory function that creates the Sentry context (span or transaction).
    is_transaction : bool, optional
        Whether this is a transaction (affects status setting behavior).

    Returns
    -------
    Callable[P, R]
        The wrapped function.
    """
    if asyncio.iscoroutinefunction(func):

        @functools.wraps(func)
        async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            start_time = time.perf_counter()

            if not sentry_sdk.is_initialized():
                return await func(*args, **kwargs)

            with context_factory() as context_obj:
                try:
                    # Set name for spans (transactions handle this themselves)
                    if not is_transaction:
                        safe_set_name(context_obj, func.__qualname__)

                    result = await func(*args, **kwargs)
                except Exception as e:
                    _handle_exception_in_sentry_context(context_obj, e)
                    raise
                else:
                    context_obj.set_status("ok")
                    return result
                finally:
                    _finalize_sentry_context(context_obj, start_time)

        return cast(Callable[P, R], async_wrapper)

    @functools.wraps(func)
    def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        start_time = time.perf_counter()

        if not sentry_sdk.is_initialized():
            return func(*args, **kwargs)

        with context_factory() as context_obj:
            try:
                # Set name for spans (transactions handle this themselves)
                if not is_transaction:
                    safe_set_name(context_obj, func.__qualname__)

                result = func(*args, **kwargs)
            except Exception as e:
                _handle_exception_in_sentry_context(context_obj, e)
                raise
            else:
                context_obj.set_status("ok")
                return result
            finally:
                _finalize_sentry_context(context_obj, start_time)

    return sync_wrapper

transaction(op: str, name: str | None = None, description: str | None = None) -> Callable[[Callable[P, R]], Callable[P, R]]

Decorator to wrap a function with a Sentry transaction.

This handles both synchronous and asynchronous functions automatically. It captures the function's execution time, sets the status to 'ok' on success or 'internal_error' on failure, and records exceptions.

Parameters:

Name Type Description Default
op str

The operation name for the transaction (e.g., 'db.query').

required
name Optional[str]

The name for the transaction. Defaults to the function's qualified name.

None
description Optional[str]

A description of what the transaction is doing.

None

Returns:

Type Description
Callable

The decorated function.

Source code in tux/utils/tracing.py
Python
def transaction(
    op: str,
    name: str | None = None,
    description: str | None = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """
    Decorator to wrap a function with a Sentry transaction.

    This handles both synchronous and asynchronous functions automatically.
    It captures the function's execution time, sets the status to 'ok' on
    success or 'internal_error' on failure, and records exceptions.

    Parameters
    ----------
    op : str
        The operation name for the transaction (e.g., 'db.query').
    name : Optional[str]
        The name for the transaction. Defaults to the function's qualified name.
    description : Optional[str]
        A description of what the transaction is doing.

    Returns
    -------
    Callable
        The decorated function.
    """

    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        # Early return if Sentry is not initialized to avoid wrapper overhead
        if not sentry_sdk.is_initialized():
            return func

        transaction_name = name or f"{func.__module__}.{func.__qualname__}"
        transaction_description = description or f"Executing {func.__qualname__}"

        def context_factory() -> Any:
            return sentry_sdk.start_transaction(
                op=op,
                name=transaction_name,
                description=transaction_description,
            )

        return create_instrumentation_wrapper(func, context_factory, is_transaction=True)

    return decorator

span(op: str, description: str | None = None) -> Callable[[Callable[P, R]], Callable[P, R]]

Decorator to wrap a function with a Sentry span.

This should be used on functions called within an existing transaction. It automatically handles both sync and async functions, captures execution time, and records success or failure status.

Parameters:

Name Type Description Default
op str

The operation name for the span (e.g., 'db.query.fetch').

required
description Optional[str]

A description of what the span is doing. Defaults to the function's name.

None

Returns:

Type Description
Callable

The decorated function.

Source code in tux/utils/tracing.py
Python
def span(op: str, description: str | None = None) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """
    Decorator to wrap a function with a Sentry span.

    This should be used on functions called within an existing transaction.
    It automatically handles both sync and async functions, captures execution
    time, and records success or failure status.

    Parameters
    ----------
    op : str
        The operation name for the span (e.g., 'db.query.fetch').
    description : Optional[str]
        A description of what the span is doing. Defaults to the function's name.

    Returns
    -------
    Callable
        The decorated function.
    """

    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        # Early return if Sentry is not initialized to avoid wrapper overhead
        if not sentry_sdk.is_initialized():
            return func

        span_description = description or f"Executing {func.__qualname__}"

        def context_factory() -> Any:
            return sentry_sdk.start_span(op=op, description=span_description)

        return create_instrumentation_wrapper(func, context_factory, is_transaction=False)

    return decorator

start_span(op: str, name: str = '') -> Generator[DummySpan | Any]

Context manager for creating a Sentry span for a block of code.

Example: with start_span("db.query", "Fetching user data"): ...

Parameters:

Name Type Description Default
op str

The operation name for the span.

required
name str

The name of the span.

''

Yields:

Type Description
Union[DummySpan, Span]

The Sentry span object or a dummy object if Sentry is not initialized.

Source code in tux/utils/tracing.py
Python
@contextmanager
def start_span(op: str, name: str = "") -> Generator[DummySpan | Any]:
    """
    Context manager for creating a Sentry span for a block of code.

    Example:
        with start_span("db.query", "Fetching user data"):
            ...

    Parameters
    ----------
    op : str
        The operation name for the span.
    name : str
        The name of the span.

    Yields
    ------
    Union[DummySpan, sentry_sdk.Span]
        The Sentry span object or a dummy object if Sentry is not initialized.
    """
    start_time = time.perf_counter()

    if not sentry_sdk.is_initialized():
        # Create a dummy context if Sentry is not available
        dummy = DummySpan()
        try:
            yield dummy
        finally:
            pass
    else:
        with sentry_sdk.start_span(op=op, name=name) as span:
            try:
                yield span
            finally:
                span.set_data("duration_ms", (time.perf_counter() - start_time) * 1000)

start_transaction(op: str, name: str, description: str = '') -> Generator[DummyTransaction | Any]

Context manager for creating a Sentry transaction for a block of code.

Example: with start_transaction("task", "process_daily_report"): ...

Parameters:

Name Type Description Default
op str

The operation name for the transaction.

required
name str

The name for the transaction.

required
description str

A description of what the transaction is doing.

''

Yields:

Type Description
Union[DummyTransaction, Transaction]

The Sentry transaction object or a dummy object if Sentry is not initialized.

Source code in tux/utils/tracing.py
Python
@contextmanager
def start_transaction(op: str, name: str, description: str = "") -> Generator[DummyTransaction | Any]:
    """
    Context manager for creating a Sentry transaction for a block of code.

    Example:
        with start_transaction("task", "process_daily_report"):
            ...

    Parameters
    ----------
    op : str
        The operation name for the transaction.
    name : str
        The name for the transaction.
    description : str
        A description of what the transaction is doing.

    Yields
    ------
    Union[DummyTransaction, sentry_sdk.Transaction]
        The Sentry transaction object or a dummy object if Sentry is not initialized.
    """
    start_time = time.perf_counter()

    if not sentry_sdk.is_initialized():
        # Create a dummy context if Sentry is not available
        dummy = DummyTransaction()
        try:
            yield dummy
        finally:
            pass
    else:
        with sentry_sdk.start_transaction(op=op, name=name, description=description) as transaction:
            try:
                yield transaction
            finally:
                transaction.set_data("duration_ms", (time.perf_counter() - start_time) * 1000)

add_tag_to_current_span(key: str, value: Any) -> None

Add a tag to the current active Sentry span, if it exists.

This is a convenience function to avoid checking for an active span everywhere in the code.

Parameters:

Name Type Description Default
key str

The key of the tag.

required
value Any

The value of the tag.

required
Source code in tux/utils/tracing.py
Python
def add_tag_to_current_span(key: str, value: Any) -> None:
    """
    Add a tag to the current active Sentry span, if it exists.

    This is a convenience function to avoid checking for an active span
    everywhere in the code.

    Parameters
    ----------
    key : str
        The key of the tag.
    value : Any
        The value of the tag.
    """
    if sentry_sdk.is_initialized() and (span := sentry_sdk.get_current_span()):
        span.set_tag(key, value)

add_data_to_current_span(key: str, value: Any) -> None

Add data to the current active Sentry span, if it exists.

This is a convenience function to attach arbitrary, non-indexed data to a span for additional context during debugging.

Parameters:

Name Type Description Default
key str

The key of the data.

required
value Any

The value of the data.

required
Source code in tux/utils/tracing.py
Python
def add_data_to_current_span(key: str, value: Any) -> None:
    """
    Add data to the current active Sentry span, if it exists.

    This is a convenience function to attach arbitrary, non-indexed data
    to a span for additional context during debugging.

    Parameters
    ----------
    key : str
        The key of the data.
    value : Any
        The value of the data.
    """
    if sentry_sdk.is_initialized() and (span := sentry_sdk.get_current_span()):
        span.set_data(key, value)

set_span_attributes(attributes: dict[str, Any]) -> None

Set multiple tags and data attributes on the current active Sentry span.

This helper function simplifies attaching context to a span by accepting a dictionary of attributes. Keys are automatically treated as tags.

Parameters:

Name Type Description Default
attributes dict[str, Any]

A dictionary where keys are the attribute names and values are the attribute values to set on the span.

required
Source code in tux/utils/tracing.py
Python
def set_span_attributes(attributes: dict[str, Any]) -> None:
    """
    Set multiple tags and data attributes on the current active Sentry span.

    This helper function simplifies attaching context to a span by accepting a
    dictionary of attributes. Keys are automatically treated as tags.

    Parameters
    ----------
    attributes : dict[str, Any]
        A dictionary where keys are the attribute names and values are the
        attribute values to set on the span.
    """
    if sentry_sdk.is_initialized() and (span := sentry_sdk.get_current_span()):
        for key, value in attributes.items():
            span.set_tag(key, value)

set_span_status(status: str, status_map: dict[str, str] | None = None) -> None

Set status on the current span.

Parameters:

Name Type Description Default
status str

The status to set (e.g., "OK", "ERROR", "NOT_FOUND")

required
status_map dict[str, str] | None

A mapping of status keys to Sentry status values. If None, uses default mapping.

None
Source code in tux/utils/tracing.py
Python
def set_span_status(status: str, status_map: dict[str, str] | None = None) -> None:
    """
    Set status on the current span.

    Parameters
    ----------
    status : str
        The status to set (e.g., "OK", "ERROR", "NOT_FOUND")
    status_map : dict[str, str] | None, optional
        A mapping of status keys to Sentry status values. If None, uses default mapping.
    """
    if not sentry_sdk.is_initialized():
        return

    if span := sentry_sdk.get_current_span():
        # Default status mapping if none provided
        if status_map is None:
            status_map = {
                "OK": "ok",
                "UNKNOWN": "unknown",
                "ERROR": "internal_error",
                "NOT_FOUND": "not_found",
                "PERMISSION_DENIED": "permission_denied",
                "INVALID_ARGUMENT": "invalid_argument",
                "RESOURCE_EXHAUSTED": "resource_exhausted",
                "UNAUTHENTICATED": "unauthenticated",
                "CANCELLED": "cancelled",
            }

        span.set_status(status_map.get(status, status))

set_setup_phase_tag(span: Any, phase: str, status: str = 'starting') -> None

Set a setup phase tag on the span.

Parameters:

Name Type Description Default
span Any

The Sentry span to tag

required
phase str

The phase name (e.g., "database", "cogs")

required
status str

The status ("starting" or "finished")

'starting'
Source code in tux/utils/tracing.py
Python
def set_setup_phase_tag(span: Any, phase: str, status: str = "starting") -> None:
    """
    Set a setup phase tag on the span.

    Parameters
    ----------
    span : Any
        The Sentry span to tag
    phase : str
        The phase name (e.g., "database", "cogs")
    status : str
        The status ("starting" or "finished")
    """
    span.set_tag("setup_phase", f"{phase}_{status}")

set_span_error(span: Any, error: Exception, error_type: str = 'error') -> None

Set error information on a span with consistent patterns.

Parameters:

Name Type Description Default
span Any

The Sentry span to set error data on

required
error Exception

The exception that occurred

required
error_type str

The type of error (e.g., "error", "discord_error", "db_error")

'error'
Source code in tux/utils/tracing.py
Python
def set_span_error(span: Any, error: Exception, error_type: str = "error") -> None:
    """
    Set error information on a span with consistent patterns.

    Parameters
    ----------
    span : Any
        The Sentry span to set error data on
    error : Exception
        The exception that occurred
    error_type : str
        The type of error (e.g., "error", "discord_error", "db_error")
    """
    span.set_status("internal_error")
    span.set_data(error_type, str(error))

capture_span_exception(exception: Exception, **extra_data: Any) -> None

Capture an exception in the current span with consistent error handling.

This consolidates the common pattern of setting span status and data when an exception occurs.

Parameters:

Name Type Description Default
exception Exception

The exception to capture.

required
**extra_data Any

Additional data to attach to the span.

{}
Source code in tux/utils/tracing.py
Python
def capture_span_exception(exception: Exception, **extra_data: Any) -> None:
    """
    Capture an exception in the current span with consistent error handling.

    This consolidates the common pattern of setting span status and data
    when an exception occurs.

    Parameters
    ----------
    exception : Exception
        The exception to capture.
    **extra_data : Any
        Additional data to attach to the span.
    """
    if sentry_sdk.is_initialized() and (span := sentry_sdk.get_current_span()):
        _handle_exception_in_sentry_context(span, exception)

        # Add any additional data
        for key, value in extra_data.items():
            span.set_data(f"extra.{key}", value)

enhanced_span(op: str, name: str = '', **initial_data: Any) -> Generator[DummySpan | Any]

Enhanced context manager for creating a Sentry span with initial data.

This extends the basic start_span with the ability to set initial tags and data, reducing boilerplate in calling code.

Parameters:

Name Type Description Default
op str

The operation name for the span.

required
name str

The name for the span.

''
**initial_data Any

Initial data to set on the span.

{}

Yields:

Type Description
Union[DummySpan, Span]

The Sentry span object or a dummy object if Sentry is not initialized.

Source code in tux/utils/tracing.py
Python
@contextmanager
def enhanced_span(op: str, name: str = "", **initial_data: Any) -> Generator[DummySpan | Any]:
    """
    Enhanced context manager for creating a Sentry span with initial data.

    This extends the basic start_span with the ability to set initial
    tags and data, reducing boilerplate in calling code.

    Parameters
    ----------
    op : str
        The operation name for the span.
    name : str
        The name for the span.
    **initial_data : Any
        Initial data to set on the span.

    Yields
    ------
    Union[DummySpan, sentry_sdk.Span]
        The Sentry span object or a dummy object if Sentry is not initialized.
    """
    # Skip spans for very short utility operations in production
    if not sentry_sdk.is_initialized():
        yield DummySpan()
        return

    # In production, skip tracing for certain frequent operations
    env = initial_data.get("environment", "development")
    if env not in ("dev", "development") and any(
        skip_term in name.lower() for skip_term in ["safe_get_attr", "connect_or_create"]
    ):
        yield DummySpan()
        return

    with start_span(op, name) as span:
        # Set initial data if provided
        if initial_data:
            for key, value in initial_data.items():
                span.set_tag(key, value)

        try:
            yield span
        except Exception as e:
            capture_span_exception(e)
            raise

instrument_bot_commands(bot: commands.Bot) -> None

Automatically instruments all bot commands with Sentry transactions.

This function iterates through all registered commands on the bot and wraps their callbacks with the @transaction decorator. This ensures that every command invocation is captured as a Sentry transaction.

Parameters:

Name Type Description Default
bot Bot

The instance of the bot whose commands should be instrumented.

required
Source code in tux/utils/tracing.py
Python
def instrument_bot_commands(bot: commands.Bot) -> None:
    """
    Automatically instruments all bot commands with Sentry transactions.

    This function iterates through all registered commands on the bot and
    wraps their callbacks with the `@transaction` decorator. This ensures
    that every command invocation is captured as a Sentry transaction.

    Parameters
    ----------
    bot : commands.Bot
        The instance of the bot whose commands should be instrumented.
    """
    # The operation for commands is standardized as `command.run`
    op = "command.run"

    for command in bot.walk_commands():
        # The transaction name is the full command name (e.g., "snippet get")
        transaction_name = f"command.{command.qualified_name}"

        # Apply the transaction decorator to the command's callback
        original_callback = cast(Callable[..., Coroutine[Any, Any, None]], command.callback)
        command.callback = transaction(op=op, name=transaction_name)(original_callback)

    logger.info(f"Instrumented {len(list(bot.walk_commands()))} commands with Sentry.")