Integrate Sentry for error tracking and enhance error handling in bot and UI interactions

This commit is contained in:
2025-01-26 03:24:38 +01:00
parent c968dc62a2
commit a08b7e7cd7
3 changed files with 131 additions and 10 deletions

View File

@ -1,13 +1,15 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import datetime import datetime
import json import json
import logging import logging
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
import discord import discord
import sentry_sdk
from apscheduler.job import Job from apscheduler.job import Job
from discord.abc import PrivateChannel from discord.abc import PrivateChannel
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
@ -18,6 +20,8 @@ from discord_reminder_bot.settings import get_bot_token, get_scheduler, get_webh
from discord_reminder_bot.ui import JobManagementView, create_job_embed from discord_reminder_bot.ui import JobManagementView, create_job_embed
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence
from apscheduler.job import Job from apscheduler.job import Job
from discord.guild import GuildChannel from discord.guild import GuildChannel
from discord.interactions import InteractionChannel from discord.interactions import InteractionChannel
@ -28,6 +32,11 @@ logger: logging.Logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logging.getLogger("discord.client").setLevel(logging.INFO) logging.getLogger("discord.client").setLevel(logging.INFO)
sentry_sdk.init(
dsn="https://c4c61a52838be9b5042144420fba5aaa@o4505228040339456.ingest.us.sentry.io/4508707268984832",
traces_sample_rate=1.0,
)
GUILD_ID = discord.Object(id=341001473661992962) GUILD_ID = discord.Object(id=341001473661992962)
scheduler: settings.AsyncIOScheduler = get_scheduler() scheduler: settings.AsyncIOScheduler = get_scheduler()
@ -46,6 +55,49 @@ class RemindBotClient(discord.Client):
super().__init__(intents=intents) super().__init__(intents=intents)
self.tree = discord.app_commands.CommandTree(self) self.tree = discord.app_commands.CommandTree(self)
async def on_error(self, event_method: str, *args: list[Any], **kwargs: dict[str, Any]) -> None:
"""Log errors that occur in the bot."""
# Log the error
logger.exception("An error occurred in %s", event_method)
# Add context to Sentry
with sentry_sdk.push_scope() as scope:
# Add event details
scope.set_tag("event_method", event_method)
scope.set_extra("args", args)
scope.set_extra("kwargs", kwargs)
# Add bot state
scope.set_tag("bot_user_id", self.user.id if self.user else "Unknown")
scope.set_tag("bot_user_name", str(self.user) if self.user else "Unknown")
scope.set_tag("bot_latency", self.latency)
# If specific arguments are available, extract and add details
if args:
interaction = next((arg for arg in args if isinstance(arg, discord.Interaction)), None)
if interaction:
scope.set_extra("interaction_id", interaction.id)
scope.set_extra("interaction_user", interaction.user.id)
scope.set_extra("interaction_user_tag", str(interaction.user))
scope.set_extra("interaction_command", interaction.command.name if interaction.command else None)
scope.set_extra("interaction_channel", str(interaction.channel))
scope.set_extra("interaction_guild", str(interaction.guild) if interaction.guild else None)
# Add Sentry tags for interaction details
scope.set_tag("interaction_id", interaction.id)
scope.set_tag("interaction_user_id", interaction.user.id)
scope.set_tag("interaction_user_tag", str(interaction.user))
scope.set_tag("interaction_command", interaction.command.name if interaction.command else "None")
scope.set_tag("interaction_channel_id", interaction.channel.id if interaction.channel else "None")
scope.set_tag("interaction_channel_name", str(interaction.channel))
scope.set_tag("interaction_guild_id", interaction.guild.id if interaction.guild else "None")
scope.set_tag("interaction_guild_name", str(interaction.guild) if interaction.guild else "None")
# Add APScheduler context
scope.set_extra("scheduler_jobs", [job.id for job in scheduler.get_jobs()])
sentry_sdk.capture_exception()
async def on_ready(self) -> None: async def on_ready(self) -> None:
"""Log when the bot is ready.""" """Log when the bot is ready."""
logger.info("Logged in as %s (%s)", self.user, self.user.id if self.user else "N/A ID") logger.info("Logged in as %s (%s)", self.user, self.user.id if self.user else "N/A ID")
@ -64,6 +116,8 @@ class RemindBotClient(discord.Client):
await msg.delete() await msg.delete()
except discord.HTTPException as e: except discord.HTTPException as e:
logger.error("Failed to remove view: %s", e) # noqa: TRY400 logger.error("Failed to remove view: %s", e) # noqa: TRY400
except asyncio.exceptions.CancelledError:
logger.error("Failed to remove view: Task was cancelled.") # noqa: TRY400
return await super().close() return await super().close()
@ -314,7 +368,6 @@ class RemindGroup(discord.app_commands.Group):
view = JobManagementView(job=jobs_in_guild[0], scheduler=scheduler, guild=guild, message=message) view = JobManagementView(job=jobs_in_guild[0], scheduler=scheduler, guild=guild, message=message)
msg_to_cleanup.append(message) msg_to_cleanup.append(message)
logger.debug("Views to cleanup: %s", msg_to_cleanup)
await interaction.followup.send(embed=embed, view=view) await interaction.followup.send(embed=embed, view=view)
@ -810,16 +863,37 @@ async def send_to_user(user_id: int, guild_id: int, message: str) -> None:
guild_id: The guild ID to get the user from. guild_id: The guild ID to get the user from.
message: The message to send. message: The message to send.
""" """
# TODO(TheLovinator): Add try/except for all of these await calls # noqa: TD003 logger.info("Sending message to user %s in guild %s:\n%s", user_id, guild_id, message)
try:
guild: discord.Guild | None = bot.get_guild(guild_id) guild: discord.Guild | None = bot.get_guild(guild_id)
if guild is None: if guild is None:
guild = await bot.fetch_guild(guild_id) guild = await bot.fetch_guild(guild_id)
except discord.NotFound:
current_guilds: Sequence[discord.Guild] = bot.guilds
logger.exception("Guild not found. Current guilds: %s", current_guilds)
return
except discord.HTTPException:
logger.exception("Failed to fetch guild")
return
try:
member: discord.Member | None = guild.get_member(user_id) member: discord.Member | None = guild.get_member(user_id)
if member is None: if member is None:
member = await guild.fetch_member(user_id) member = await guild.fetch_member(user_id)
except discord.Forbidden:
logger.exception("We do not have access to the guild. Guild: %s, User: %s", guild_id, user_id)
return
except discord.NotFound:
logger.exception("Member not found. Guild: %s, User: %s", guild_id, user_id)
return
except discord.HTTPException:
logger.exception("Fetching the member failed. Guild: %s, User: %s", guild_id, user_id)
return
try:
await member.send(message) await member.send(message)
except discord.HTTPException:
logger.exception("Failed to send message (%s) to user (%s) in guild (%s)", message, user_id, guild_id)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,6 +4,7 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import discord import discord
import sentry_sdk
from apscheduler.job import Job from apscheduler.job import Job
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
@ -262,6 +263,46 @@ class JobManagementView(discord.ui.View):
logger.debug("No message to edit for job: %s", self.job.id) logger.debug("No message to edit for job: %s", self.job.id)
self.stop() self.stop()
async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None:
"""Handle errors that occur within the view.
Args:
interaction: The interaction object for the command.
error: The exception that was raised.
item: The item that caused the error.
"""
with sentry_sdk.push_scope() as scope:
# Interaction-related context
scope.set_extra("interaction_id", interaction.id)
scope.set_extra("interaction_user", interaction.user.id)
scope.set_extra("interaction_user_tag", str(interaction.user))
scope.set_extra("interaction_command", interaction.command.name if interaction.command else None)
scope.set_extra("interaction_channel", str(interaction.channel))
scope.set_extra("interaction_guild", str(interaction.guild) if interaction.guild else None)
# Item-related context
scope.set_extra("item_type", type(item).__name__)
scope.set_extra("item_label", getattr(item, "label", None))
# Job and scheduler context
scope.set_extra("job_id", self.job.id if self.job else None)
scope.set_extra("job_kwargs", self.job.kwargs if self.job else None)
# Guild and message context
scope.set_extra("guild_id", self.guild.id if self.guild else None)
scope.set_extra("guild_name", self.guild.name if self.guild else None)
scope.set_extra("message_id", self.message.id if self.message else None)
scope.set_extra("message_content", self.message.content if self.message else None)
# Tags for categorization
scope.set_tag("error_type", type(error).__name__)
scope.set_tag("interaction_type", "command" if interaction.command else "other")
if interaction.guild:
scope.set_tag("guild_id", interaction.guild.id)
scope.set_tag("guild_name", interaction.guild.name)
sentry_sdk.capture_exception(error)
@discord.ui.button(label="Delete", style=discord.ButtonStyle.danger) @discord.ui.button(label="Delete", style=discord.ButtonStyle.danger)
async def delete_button(self, interaction: discord.Interaction, button: Button) -> None: # noqa: ARG002 async def delete_button(self, interaction: discord.Interaction, button: Button) -> None: # noqa: ARG002
"""Delete the job. """Delete the job.

View File

@ -27,6 +27,9 @@ dependencies = [
# For loading environment variables from a .env file # For loading environment variables from a .env file
"python-dotenv>=1.0.1,<2.0.0", # https://github.com/theskumar/python-dotenv "python-dotenv>=1.0.1,<2.0.0", # https://github.com/theskumar/python-dotenv
# For error tracking
"sentry-sdk>=2.20.0,<3.0.0", # https://github.com/getsentry/sentry-python
] ]
[dependency-groups] [dependency-groups]
@ -72,6 +75,9 @@ discord-webhook = {version = ">=1.3.1,<2.0.0"}
# For loading environment variables from a .env file # For loading environment variables from a .env file
python-dotenv = {version = ">=1.0.1,<2.0.0"} python-dotenv = {version = ">=1.0.1,<2.0.0"}
# For error tracking
sentry-sdk = {version = ">=2.20.0,<3.0.0"}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "*" pytest = "*"
pre-commit = "*" pre-commit = "*"