11import asyncio
22import contextlib
3+ import types
34from sys import exception
45
56import aiohttp
67from discord .errors import Forbidden
8+ from discord .ext import commands
79from pydis_core import BotBase
10+ from pydis_core .utils import scheduling
11+ from pydis_core .utils ._extensions import walk_extensions
812from pydis_core .utils .error_handling import handle_forbidden_from_block
913from sentry_sdk import new_scope , start_transaction
1014
1115from bot import constants , exts
1216from bot .log import get_logger
17+ from bot .utils .startup_reporting import StartupFailureReporter
1318
1419log = get_logger ("bot" )
1520
16-
1721class StartupError (Exception ):
1822 """Exception class for startup errors."""
1923
@@ -26,9 +30,13 @@ class Bot(BotBase):
2630 """A subclass of `pydis_core.BotBase` that implements bot-specific functions."""
2731
2832 def __init__ (self , * args , ** kwargs ):
29-
3033 super ().__init__ (* args , ** kwargs )
3134
35+ # Track extension load failures and tasks so we can report them after all attempts have completed
36+ self .extension_load_failures : dict [str , BaseException ] = {}
37+ self ._extension_load_tasks : dict [str , asyncio .Task ] = {}
38+ self ._startup_failure_reporter = StartupFailureReporter ()
39+
3240 async def load_extension (self , name : str , * args , ** kwargs ) -> None :
3341 """Extend D.py's load_extension function to also record sentry performance stats."""
3442 with start_transaction (op = "cog-load" , name = name ):
@@ -77,3 +85,53 @@ async def on_error(self, event: str, *args, **kwargs) -> None:
7785 scope .set_extra ("kwargs" , kwargs )
7886
7987 log .exception (f"Unhandled exception in { event } ." )
88+
89+ async def add_cog (self , cog : commands .Cog ) -> None :
90+ """
91+ Add a cog to the bot with exception handling.
92+
93+ Override of `BotBase.add_cog` to capture and log any exceptions raised during cog loading,
94+ including the extension name if available.
95+ """
96+ extension = cog .__module__
97+
98+ try :
99+ await super ().add_cog (cog )
100+ log .info (f"Cog successfully loaded: { cog .qualified_name } " )
101+
102+ except BaseException as e :
103+ key = extension or f"(unknown)::{ cog .qualified_name } "
104+ self .extension_load_failures [key ] = e
105+
106+ log .exception (
107+ f"Failed during add_cog (extension={ extension } , cog={ cog .qualified_name } )"
108+ )
109+ # Propagate error
110+ raise
111+
112+ async def _load_extensions (self , module : types .ModuleType ) -> None :
113+ """Load extensions for the bot."""
114+ await self .wait_until_guild_available ()
115+
116+ self .all_extensions = walk_extensions (module )
117+
118+ async def _load_one (extension : str ) -> None :
119+ try :
120+ await self .load_extension (extension )
121+ log .info (f"Extension successfully loaded: { extension } " )
122+
123+ except BaseException as e :
124+ self .extension_load_failures [extension ] = e
125+ log .exception (f"Failed to load extension: { extension } " )
126+ raise
127+
128+ for extension in self .all_extensions :
129+ task = scheduling .create_task (_load_one (extension ))
130+ self ._extension_load_tasks [extension ] = task
131+
132+ # Wait for all load tasks to complete so we can report any failures together
133+ await asyncio .gather (* self ._extension_load_tasks .values (), return_exceptions = True )
134+
135+ # Send a Discord message to moderators if any extensions failed to load
136+ if self .extension_load_failures :
137+ await self ._startup_failure_reporter .notify (self , self .extension_load_failures )
0 commit comments