From 8dda0a8ffff73bc61ea5aa53af449667f726eb57 Mon Sep 17 00:00:00 2001 From: Nikita Grover Date: Sat, 18 Jul 2026 17:45:24 +0530 Subject: [PATCH 1/3] Add discord.py instrumentation Instruments discord.py prefix commands and app commands via BaseInstrumentor, wrapping Bot.invoke and CommandTree._call. Relates to #4492 --- .../instrumentation/discord_py/discord_py.rst | 7 + .../README.rst | 23 +++ .../pyproject.toml | 54 ++++++ .../instrumentation/discord_py/__init__.py | 157 ++++++++++++++++++ .../instrumentation/discord_py/package.py | 4 + .../instrumentation/discord_py/version.py | 4 + .../test-requirements.txt | 5 + .../tests/__init__.py | 2 + .../tests/test_discord_py_instrumentation.py | 121 ++++++++++++++ tox.ini | 11 ++ 10 files changed, 388 insertions(+) create mode 100644 docs/instrumentation/discord_py/discord_py.rst create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/pyproject.toml create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/test-requirements.txt create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py diff --git a/docs/instrumentation/discord_py/discord_py.rst b/docs/instrumentation/discord_py/discord_py.rst new file mode 100644 index 0000000000..d9da04f259 --- /dev/null +++ b/docs/instrumentation/discord_py/discord_py.rst @@ -0,0 +1,7 @@ +OpenTelemetry discord.py Instrumentation +========================================= + +.. automodule:: opentelemetry.instrumentation.discord_py + :members: + :undoc-members: + :show-inheritance: diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/README.rst b/instrumentation/opentelemetry-instrumentation-discord-py/README.rst new file mode 100644 index 0000000000..9fb832c3d3 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry discord.py Instrumentation +========================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-discord-py.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-discord-py/ + +This library allows tracing requests made by discord.py bots (prefix +commands and application/slash commands). + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-discord-py + +References +---------- + +* `OpenTelemetry discord.py Instrumentation `_ +* `OpenTelemetry Project `_ diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/pyproject.toml b/instrumentation/opentelemetry-instrumentation-discord-py/pyproject.toml new file mode 100644 index 0000000000..55434917dc --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-instrumentation-discord-py" +dynamic = ["version"] +description = "OpenTelemetry discord.py instrumentation" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.10" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "opentelemetry-api ~= 1.12", + "opentelemetry-instrumentation == 0.65b0.dev", + "wrapt >= 1.0.0, < 2.0.0", +] + +[project.optional-dependencies] +instruments = [ + "discord.py >= 2.0", +] + +[project.entry-points.opentelemetry_instrumentor] +discord_py = "opentelemetry.instrumentation.discord_py:DiscordPyInstrumentor" + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-discord-py" +Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" + +[tool.hatch.version] +path = "src/opentelemetry/instrumentation/discord_py/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/__init__.py b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/__init__.py new file mode 100644 index 0000000000..6cb543df4d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/__init__.py @@ -0,0 +1,157 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +""" +This library allows tracing discord.py bots: prefix commands (``ext.commands``) +and application/slash commands (``discord.app_commands``) are wrapped in spans +automatically once instrumented. + +Usage +----- + +.. code-block:: python + + from opentelemetry.instrumentation.discord_py import DiscordPyInstrumentor + + DiscordPyInstrumentor().instrument() + + # ... create and run your discord.py Bot as usual + +Every prefix command invocation produces a span named ``discord.command`` and +every application (slash) command invocation produces a span named +``discord.app_command``. Both carry ``discord.command.name``, +``discord.guild.id``, ``discord.channel.id`` and ``discord.user.id`` as +attributes. + +API +--- +""" + +# pylint: disable=no-name-in-module,no-self-use + +from typing import Collection + +import discord.app_commands.tree +import discord.ext.commands.bot +import wrapt + +from opentelemetry.instrumentation.discord_py.package import _instruments +from opentelemetry.instrumentation.discord_py.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer + +_PREFIX_COMMAND_SPAN_NAME = "discord.command" +_APP_COMMAND_SPAN_NAME = "discord.app_command" + + +def _set_common_attributes(span, *, guild_id, channel_id, user_id, command_name): + """Attach the low-cardinality attributes every discord.py span carries. + + ``None`` values are dropped rather than set, so call sites can pass + optional discord.py fields (e.g. ``guild_id`` on a DM interaction) + without branching. + """ + if command_name is not None: + span.set_attribute("discord.command.name", command_name) + if guild_id is not None: + span.set_attribute("discord.guild.id", guild_id) + if channel_id is not None: + span.set_attribute("discord.channel.id", channel_id) + if user_id is not None: + span.set_attribute("discord.user.id", user_id) + + +class DiscordPyInstrumentor(BaseInstrumentor): + """An instrumentor for discord.py. + + See `BaseInstrumentor`. + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer( + __name__, + __version__, + tracer_provider, + ) + + wrapt.wrap_function_wrapper( + "discord.ext.commands.bot", + "Bot.invoke", + _wrap_prefix_command_invoke(tracer), + ) + wrapt.wrap_function_wrapper( + "discord.app_commands.tree", + "CommandTree._call", + _wrap_app_command_call(tracer), + ) + + def _uninstrument(self, **kwargs): + unwrap(discord.ext.commands.bot.Bot, "invoke") + unwrap(discord.app_commands.tree.CommandTree, "_call") + + +def _wrap_prefix_command_invoke(tracer): + async def wrapper(wrapped, instance, args, kwargs): + # signature: Bot.invoke(self, ctx) + ctx = args[0] if args else kwargs.get("ctx") + command = getattr(ctx, "command", None) + command_name = command.qualified_name if command else None + + with tracer.start_as_current_span( + _PREFIX_COMMAND_SPAN_NAME, + kind=SpanKind.SERVER, + ) as span: + if span.is_recording(): + _set_common_attributes( + span, + guild_id=getattr(getattr(ctx, "guild", None), "id", None), + channel_id=getattr(getattr(ctx, "channel", None), "id", None), + user_id=getattr(getattr(ctx, "author", None), "id", None), + command_name=command_name, + ) + try: + return await wrapped(*args, **kwargs) + except Exception as exc: + span.set_status(Status(StatusCode.ERROR, str(exc))) + span.record_exception(exc) + raise + + return wrapper + + +def _wrap_app_command_call(tracer): + async def wrapper(wrapped, instance, args, kwargs): + # signature: CommandTree._call(self, interaction) + interaction = args[0] if args else kwargs.get("interaction") + + with tracer.start_as_current_span( + _APP_COMMAND_SPAN_NAME, + kind=SpanKind.SERVER, + ) as span: + try: + return await wrapped(*args, **kwargs) + finally: + # The command isn't resolved on the interaction until + # after ``_call`` runs its internal lookup, so read it back + # out afterwards rather than before invoking ``wrapped``. + if span.is_recording(): + command = getattr(interaction, "command", None) + command_name = ( + command.qualified_name if command else None + ) + _set_common_attributes( + span, + guild_id=getattr(interaction, "guild_id", None), + channel_id=getattr(interaction, "channel_id", None), + user_id=getattr( + getattr(interaction, "user", None), "id", None + ), + command_name=command_name, + ) + + return wrapper diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/package.py b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/package.py new file mode 100644 index 0000000000..6b00b17974 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/package.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +_instruments = ("discord.py >= 2.0",) diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/version.py b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/version.py new file mode 100644 index 0000000000..3716e78195 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/src/opentelemetry/instrumentation/discord_py/version.py @@ -0,0 +1,4 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "0.1.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-discord-py/test-requirements.txt new file mode 100644 index 0000000000..c184b78608 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/test-requirements.txt @@ -0,0 +1,5 @@ +discord.py==2.4.0 +pytest==7.4.4 +wrapt==1.16.0 +-e opentelemetry-instrumentation +-e instrumentation/opentelemetry-instrumentation-discord-py diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-discord-py/tests/__init__.py new file mode 100644 index 0000000000..e57cf4aba9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py b/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py new file mode 100644 index 0000000000..acc41ac7fe --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py @@ -0,0 +1,121 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=invalid-name,no-name-in-module + +import asyncio +from unittest import mock + +import discord.app_commands.tree +import discord.ext.commands.bot + +from opentelemetry.instrumentation.discord_py import DiscordPyInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind, StatusCode + + +def _run(coro): + return asyncio.run(coro) + + +class TestDiscordPyInstrumentor(TestBase): + def setUp(self): + super().setUp() + DiscordPyInstrumentor().instrument(tracer_provider=self.tracer_provider) + + def tearDown(self): + super().tearDown() + DiscordPyInstrumentor().uninstrument() + + def test_prefix_command_creates_span(self): + ctx = mock.Mock() + ctx.command.qualified_name = "ping" + ctx.guild.id = 111 + ctx.channel.id = 222 + ctx.author.id = 333 + + async def fake_invoke(self, ctx): + return None + + with mock.patch.object( + discord.ext.commands.bot.Bot, "invoke", new=fake_invoke + ): + # Re-instrument so the patch applied above is what gets wrapped. + DiscordPyInstrumentor().uninstrument() + DiscordPyInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + fake_bot = discord.ext.commands.bot.Bot.__new__( + discord.ext.commands.bot.Bot + ) + _run(fake_bot.invoke(ctx)) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "discord.command") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.attributes["discord.command.name"], "ping") + self.assertEqual(span.attributes["discord.guild.id"], 111) + self.assertEqual(span.attributes["discord.channel.id"], 222) + self.assertEqual(span.attributes["discord.user.id"], 333) + + def test_prefix_command_records_exception(self): + ctx = mock.Mock() + ctx.command = None + ctx.guild = None + ctx.channel.id = 222 + ctx.author.id = 333 + + async def failing_invoke(self, ctx): + raise RuntimeError("boom") + + with mock.patch.object( + discord.ext.commands.bot.Bot, "invoke", new=failing_invoke + ): + DiscordPyInstrumentor().uninstrument() + DiscordPyInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + fake_bot = discord.ext.commands.bot.Bot.__new__( + discord.ext.commands.bot.Bot + ) + with self.assertRaises(RuntimeError): + _run(fake_bot.invoke(ctx)) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) + + def test_app_command_creates_span(self): + interaction = mock.Mock() + interaction.command.qualified_name = "slash-ping" + interaction.guild_id = 444 + interaction.channel_id = 555 + interaction.user.id = 666 + + async def fake_call(self, interaction): + return None + + with mock.patch.object( + discord.app_commands.tree.CommandTree, "_call", new=fake_call + ): + DiscordPyInstrumentor().uninstrument() + DiscordPyInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + fake_tree = discord.app_commands.tree.CommandTree.__new__( + discord.app_commands.tree.CommandTree + ) + _run(fake_tree._call(interaction)) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "discord.app_command") + self.assertEqual( + span.attributes["discord.command.name"], "slash-ping" + ) + self.assertEqual(span.attributes["discord.guild.id"], 444) + self.assertEqual(span.attributes["discord.channel.id"], 555) + self.assertEqual(span.attributes["discord.user.id"], 666) diff --git a/tox.ini b/tox.ini index 81ba52d1f5..29ab8d96ac 100644 --- a/tox.ini +++ b/tox.ini @@ -206,6 +206,11 @@ envlist = lint-instrumentation-logging benchmark-instrumentation-logging + ; opentelemetry-instrumentation-discord-py + py3{10,11,12,13,14}-test-instrumentation-discord_py + pypy3-test-instrumentation-discord_py + lint-instrumentation-discord_py + ; opentelemetry-instrumentation-exceptions py3{10,11,12,13,14}-test-instrumentation-exceptions pypy3-test-instrumentation-exceptions @@ -710,6 +715,9 @@ deps = logging: {[testenv]test_deps} logging: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/test-requirements.txt benchmark-instrumentation-logging: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/benchmark-requirements.txt + + discord_py: {[testenv]test_deps} + discord_py: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-discord-py/test-requirements.txt exceptions: {[testenv]test_deps} exceptions: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-exceptions/test-requirements.txt @@ -920,6 +928,9 @@ commands = lint-instrumentation-logging: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-logging" benchmark-instrumentation-logging: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-logging/benchmarks {posargs} --benchmark-json=instrumentation-logging-benchmark.json + test-instrumentation-discord_py: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-discord-py/tests {posargs} + lint-instrumentation-discord_py: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-discord-py" + test-instrumentation-exceptions: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-exceptions/tests {posargs} lint-instrumentation-exceptions: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-exceptions" From 1fd8d04de4d1c6af5b4605d2466cbc054be860e6 Mon Sep 17 00:00:00 2001 From: Nikita Grover Date: Mon, 20 Jul 2026 14:11:37 +0530 Subject: [PATCH 2/3] Add changelog fragment, fix docs template, regen bootstrap - Docs stub now matches the official _template/autodoc_entry.rst (includes README content before the automodule directive) - Add .changelog/4842.added towncrier fragment - Regenerate bootstrap_gen.py to register discord.py for auto-instrumentation --- .changelog/4842.added | 1 + docs/instrumentation/discord_py/discord_py.rst | 4 ++-- .../src/opentelemetry/instrumentation/bootstrap_gen.py | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changelog/4842.added diff --git a/.changelog/4842.added b/.changelog/4842.added new file mode 100644 index 0000000000..8a8f325280 --- /dev/null +++ b/.changelog/4842.added @@ -0,0 +1 @@ +`opentelemetry-instrumentation-discord-py`: add prefix and app command tracing diff --git a/docs/instrumentation/discord_py/discord_py.rst b/docs/instrumentation/discord_py/discord_py.rst index d9da04f259..2d5c126a27 100644 --- a/docs/instrumentation/discord_py/discord_py.rst +++ b/docs/instrumentation/discord_py/discord_py.rst @@ -1,5 +1,5 @@ -OpenTelemetry discord.py Instrumentation -========================================= +.. include:: ../../../instrumentation/opentelemetry-instrumentation-discord-py/README.rst + :end-before: References .. automodule:: opentelemetry.instrumentation.discord_py :members: diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index ac9adc84a3..66975d965f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -77,6 +77,10 @@ "library": "confluent-kafka >= 1.8.2, < 3.0.0", "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.65b0.dev", }, + { + "library": "discord.py >= 2.0", + "instrumentation": "opentelemetry-instrumentation-discord-py==0.1.dev0", + }, { "library": "django >= 2.0", "instrumentation": "opentelemetry-instrumentation-django==0.65b0.dev", From f992f6b0ce591648c103ccdc692c3924400b6fbe Mon Sep 17 00:00:00 2001 From: Nikita Grover Date: Mon, 20 Jul 2026 14:18:36 +0530 Subject: [PATCH 3/3] Use IsolatedAsyncioTestCase for async test isolation Per CONTRIBUTING.md guidance, async tests should inherit IsolatedAsyncioTestCase rather than being manually driven via asyncio.run(), which can lead to tests passing without properly exercising the async code path. --- .../tests/test_discord_py_instrumentation.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py b/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py index acc41ac7fe..17813483ab 100644 --- a/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-discord-py/tests/test_discord_py_instrumentation.py @@ -3,8 +3,7 @@ # pylint: disable=invalid-name,no-name-in-module -import asyncio -from unittest import mock +from unittest import IsolatedAsyncioTestCase, mock import discord.app_commands.tree import discord.ext.commands.bot @@ -14,11 +13,7 @@ from opentelemetry.trace import SpanKind, StatusCode -def _run(coro): - return asyncio.run(coro) - - -class TestDiscordPyInstrumentor(TestBase): +class TestDiscordPyInstrumentor(TestBase, IsolatedAsyncioTestCase): def setUp(self): super().setUp() DiscordPyInstrumentor().instrument(tracer_provider=self.tracer_provider) @@ -27,7 +22,7 @@ def tearDown(self): super().tearDown() DiscordPyInstrumentor().uninstrument() - def test_prefix_command_creates_span(self): + async def test_prefix_command_creates_span(self): ctx = mock.Mock() ctx.command.qualified_name = "ping" ctx.guild.id = 111 @@ -48,7 +43,7 @@ async def fake_invoke(self, ctx): fake_bot = discord.ext.commands.bot.Bot.__new__( discord.ext.commands.bot.Bot ) - _run(fake_bot.invoke(ctx)) + await fake_bot.invoke(ctx) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -60,7 +55,7 @@ async def fake_invoke(self, ctx): self.assertEqual(span.attributes["discord.channel.id"], 222) self.assertEqual(span.attributes["discord.user.id"], 333) - def test_prefix_command_records_exception(self): + async def test_prefix_command_records_exception(self): ctx = mock.Mock() ctx.command = None ctx.guild = None @@ -81,13 +76,13 @@ async def failing_invoke(self, ctx): discord.ext.commands.bot.Bot ) with self.assertRaises(RuntimeError): - _run(fake_bot.invoke(ctx)) + await fake_bot.invoke(ctx) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) self.assertEqual(spans[0].status.status_code, StatusCode.ERROR) - def test_app_command_creates_span(self): + async def test_app_command_creates_span(self): interaction = mock.Mock() interaction.command.qualified_name = "slash-ping" interaction.guild_id = 444 @@ -107,7 +102,7 @@ async def fake_call(self, interaction): fake_tree = discord.app_commands.tree.CommandTree.__new__( discord.app_commands.tree.CommandTree ) - _run(fake_tree._call(interaction)) + await fake_tree._call(interaction) spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1)