mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 11:33:50 +08:00
refactor: Expand better-error-logging patterns across the codebase (#15241)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
co-authored by
openhands
parent
93c0871951
commit
7db8bd4d92
@@ -230,8 +230,8 @@ class AzureDevOpsManager(Manager[AzureDevOpsViewType]):
|
||||
|
||||
await self.send_message(msg_info, azure_view)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'[Azure DevOps] Error starting job: {e}')
|
||||
except Exception:
|
||||
logger.exception('[Azure DevOps] Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(',
|
||||
azure_view,
|
||||
|
||||
@@ -142,14 +142,15 @@ class AzureDevOpsV1CallbackProcessor(EventCallbackProcessor):
|
||||
error_detail += f': {e.response.text}'
|
||||
except Exception:
|
||||
pass
|
||||
_logger.error(
|
||||
'[Azure DevOps V1] HTTP error: %s', error_detail, exc_info=True
|
||||
_logger.exception(
|
||||
'[Azure DevOps V1] HTTP error',
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
raise Exception('Failed to send message to agent server') from e
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f'Request timeout after 30 seconds to {url}')
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f'Request error to {url}: {e}')
|
||||
raise Exception(f'Request error to {url}') from e
|
||||
|
||||
async def _request_summary(self, conversation_id: UUID) -> str:
|
||||
from openhands.app_server.config import (
|
||||
|
||||
@@ -256,8 +256,8 @@ class BitbucketManager(Manager[BitbucketViewType]):
|
||||
|
||||
await self.send_message(msg_info, bitbucket_view)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'[Bitbucket] Error starting job: {e}')
|
||||
except Exception:
|
||||
logger.exception('[Bitbucket] Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(',
|
||||
bitbucket_view,
|
||||
|
||||
@@ -132,12 +132,17 @@ class BitbucketV1CallbackProcessor(EventCallbackProcessor):
|
||||
error_detail += f': {e.response.text}'
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
_logger.error('[Bitbucket V1] HTTP error: %s', error_detail, exc_info=True)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
_logger.exception(
|
||||
'[Bitbucket V1] HTTP error',
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(
|
||||
f'Failed to send message to agent server: {error_detail}'
|
||||
) from e
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f'Request timeout after 30 seconds to {url}')
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f'Request error to {url}: {e}')
|
||||
raise Exception(f'Request error to {url}') from e
|
||||
|
||||
async def _request_summary(self, conversation_id: UUID) -> str:
|
||||
from openhands.app_server.config import (
|
||||
|
||||
@@ -443,8 +443,8 @@ class BitbucketDCManager(Manager[BitbucketDCViewType]):
|
||||
|
||||
await self.send_message(msg_info, bitbucket_view)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'[Bitbucket DC] Error starting job: {e}')
|
||||
except Exception:
|
||||
logger.exception('[Bitbucket DC] Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(',
|
||||
bitbucket_view,
|
||||
|
||||
@@ -132,14 +132,17 @@ class BitbucketDCV1CallbackProcessor(EventCallbackProcessor):
|
||||
error_detail += f': {e.response.text}'
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
_logger.error(
|
||||
'[Bitbucket DC V1] HTTP error: %s', error_detail, exc_info=True
|
||||
_logger.exception(
|
||||
'[Bitbucket DC V1] HTTP error',
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
raise Exception(
|
||||
f'Failed to send message to agent server: {error_detail}'
|
||||
) from e
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f'Request timeout after 30 seconds to {url}')
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f'Request error to {url}: {e}')
|
||||
raise Exception(f'Request error to {url}') from e
|
||||
|
||||
async def _request_summary(self, conversation_id: UUID) -> str:
|
||||
from openhands.app_server.config import (
|
||||
|
||||
@@ -552,7 +552,7 @@ class GitHubDataCollector:
|
||||
break
|
||||
|
||||
except Exception:
|
||||
logger.warning('Error fetching PR data', exc_info=True)
|
||||
logger.warning('Error fetching PR data')
|
||||
return
|
||||
|
||||
if not pr_data or not repo_data:
|
||||
|
||||
@@ -199,10 +199,11 @@ class GithubManager(Manager[GithubViewType]):
|
||||
repo = github_client.get_repo(full_repo_name)
|
||||
issue = repo.get_issue(number=issue_number)
|
||||
issue.create_comment(get_user_not_found_message(username))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'[GitHub] Failed to send user not found message to {username} '
|
||||
f'on {full_repo_name}#{issue_number}: {e}'
|
||||
f'on {full_repo_name}#{issue_number}',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
async def is_job_requested(self, message: Message) -> bool:
|
||||
@@ -262,8 +263,9 @@ class GithubManager(Manager[GithubViewType]):
|
||||
try:
|
||||
await self.data_collector.process_payload(message)
|
||||
except Exception:
|
||||
logger.error(
|
||||
'[Github]: Error processing payload for gh interaction', exc_info=True
|
||||
logger.exception(
|
||||
'[Github]: Error processing payload for gh interaction',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
if await self.is_job_requested(message):
|
||||
@@ -429,7 +431,7 @@ class GithubManager(Manager[GithubViewType]):
|
||||
await self.send_message(msg_info, github_view)
|
||||
|
||||
except Exception:
|
||||
logger.exception('[Github]: Error starting job')
|
||||
logger.exception('[Github]: Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', github_view
|
||||
)
|
||||
|
||||
@@ -153,9 +153,9 @@ class SaaSGitHubService(GitHubService):
|
||||
f'Determined external_auth_id from Keycloak token: {self.external_auth_id}'
|
||||
)
|
||||
return self.external_auth_id
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.warning(
|
||||
f'Could not determine external_auth_id from token: {e}',
|
||||
'Could not determine external_auth_id from token',
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -207,27 +207,27 @@ class GithubV1CallbackProcessor(EventCallbackProcessor):
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'[GitHub V1] HTTP error fetching final response from %s: %s. '
|
||||
'Response headers: %s',
|
||||
url,
|
||||
error_detail,
|
||||
dict(e.response.headers),
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(
|
||||
f'Failed to fetch final response from agent server: {error_detail}'
|
||||
)
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_detail = f'Request timeout after 30 seconds to {url}'
|
||||
_logger.error('[GitHub V1] %s', error_detail, exc_info=True)
|
||||
_logger.exception('[GitHub V1] %s', error_detail, stack_info=True)
|
||||
raise Exception(error_detail)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
error_detail = f'Request error to {url}: {str(e)}'
|
||||
_logger.error('[GitHub V1] %s', error_detail, exc_info=True)
|
||||
raise Exception(error_detail)
|
||||
error_detail = f'Request error to {url}'
|
||||
_logger.exception('[GitHub V1] %s', error_detail, stack_info=True)
|
||||
raise Exception(error_detail) from e
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Final response orchestration
|
||||
|
||||
@@ -257,8 +257,8 @@ class GitlabManager(Manager[GitlabViewType]):
|
||||
# Send the acknowledgment message
|
||||
await self.send_message(msg_info, gitlab_view)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'[GitLab] Error starting job: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('[GitLab] Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', gitlab_view
|
||||
)
|
||||
|
||||
@@ -567,8 +567,8 @@ class SaaSGitLabService(GitLabService):
|
||||
params = {'body': body}
|
||||
|
||||
await self._make_request(url=url, params=params, method=RequestMethod.POST)
|
||||
except Exception as e:
|
||||
logger.exception(f'[GitLab]: Reply to issue failed {e}')
|
||||
except Exception:
|
||||
logger.exception('[GitLab]: Reply to issue failed', stack_info=True)
|
||||
|
||||
async def reply_to_mr(
|
||||
self, project_id: str, merge_request_iid: str, discussion_id: str, body: str
|
||||
@@ -581,8 +581,8 @@ class SaaSGitLabService(GitLabService):
|
||||
params = {'body': body}
|
||||
|
||||
await self._make_request(url=url, params=params, method=RequestMethod.POST)
|
||||
except Exception as e:
|
||||
logger.exception(f'[GitLab]: Reply to MR failed {e}')
|
||||
except Exception:
|
||||
logger.exception('[GitLab]: Reply to MR failed', stack_info=True)
|
||||
|
||||
async def get_user_resources_with_admin_access(
|
||||
self,
|
||||
|
||||
@@ -180,36 +180,38 @@ class GitlabV1CallbackProcessor(EventCallbackProcessor):
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'[GitLab V1] HTTP error sending message to %s: %s. '
|
||||
'Request payload: %s. Response headers: %s',
|
||||
url,
|
||||
error_detail,
|
||||
payload,
|
||||
dict(e.response.headers),
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
raise Exception(
|
||||
f'Failed to send message to agent server: {error_detail}'
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_detail = f'Request timeout after 30 seconds to {url}'
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'[GitLab V1] %s. Request payload: %s',
|
||||
error_detail,
|
||||
payload,
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(error_detail)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
error_detail = f'Request error to {url}: {str(e)}'
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'[GitLab V1] %s. Request payload: %s',
|
||||
error_detail,
|
||||
payload,
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(error_detail)
|
||||
raise Exception(error_detail) from e
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Summary orchestration
|
||||
|
||||
@@ -155,11 +155,13 @@ class JiraManager(Manager[JiraViewInterface]):
|
||||
)
|
||||
await self._send_error_from_payload(payload, workspace, str(e))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira] Unexpected error creating view',
|
||||
extra={'issue_key': payload.issue_key, 'error': str(e)},
|
||||
exc_info=True,
|
||||
extra={
|
||||
'issue_key': payload.issue_key,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
await self._send_error_from_payload(
|
||||
payload,
|
||||
@@ -312,11 +314,13 @@ class JiraManager(Manager[JiraViewInterface]):
|
||||
)
|
||||
msg_info = str(e)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira] Unexpected error starting job',
|
||||
extra={'issue_key': view.payload.issue_key, 'error': str(e)},
|
||||
exc_info=True,
|
||||
extra={
|
||||
'issue_key': view.payload.issue_key,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
msg_info = 'Sorry, there was an unexpected error starting the job. Please try again.'
|
||||
|
||||
@@ -366,10 +370,13 @@ class JiraManager(Manager[JiraViewInterface]):
|
||||
svc_acc_email=view.jira_workspace.svc_acc_email,
|
||||
svc_acc_api_key=api_key,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira] Failed to send comment',
|
||||
extra={'issue_key': view.payload.issue_key, 'error': str(e)},
|
||||
extra={
|
||||
'issue_key': view.payload.issue_key,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
async def _send_error_from_payload(
|
||||
@@ -388,10 +395,13 @@ class JiraManager(Manager[JiraViewInterface]):
|
||||
svc_acc_email=workspace.svc_acc_email,
|
||||
svc_acc_api_key=api_key,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira] Failed to send error comment',
|
||||
extra={'issue_key': payload.issue_key, 'error': str(e)},
|
||||
extra={
|
||||
'issue_key': payload.issue_key,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
def get_workspace_name_from_payload(self, payload: dict) -> str | None:
|
||||
|
||||
@@ -86,7 +86,7 @@ class JiraV1CallbackProcessor(EventCallbackProcessor):
|
||||
detail=summary,
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.exception(f'[Jira] Failed to post summary: {e}', stack_info=True)
|
||||
_logger.exception('[Jira] Failed to post summary', stack_info=True)
|
||||
return EventCallbackResult(
|
||||
status=EventCallbackResultStatus.ERROR,
|
||||
event_callback_id=callback.id,
|
||||
@@ -199,7 +199,9 @@ class JiraV1CallbackProcessor(EventCallbackProcessor):
|
||||
dict(e.response.headers),
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
raise Exception(
|
||||
f'Failed to send message to agent server: {error_detail}'
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_detail = (
|
||||
|
||||
@@ -130,24 +130,26 @@ class JiraNewConversationView(JiraViewInterface):
|
||||
return self._issue_title, self._issue_description
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'[Jira] Failed to fetch issue details',
|
||||
extra={
|
||||
'issue_key': self.payload.issue_key,
|
||||
'status': e.response.status_code,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise StartingConvoException(
|
||||
f'Failed to fetch issue details: HTTP {e.response.status_code}'
|
||||
)
|
||||
raise StartingConvoException('Failed to fetch issue details: HTTP') from e
|
||||
except Exception as e:
|
||||
if isinstance(e, StartingConvoException):
|
||||
raise
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'[Jira] Failed to fetch issue details',
|
||||
extra={'issue_key': self.payload.issue_key, 'error': str(e)},
|
||||
extra={
|
||||
'issue_key': self.payload.issue_key,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise StartingConvoException(f'Failed to fetch issue details: {str(e)}')
|
||||
raise StartingConvoException('Failed to fetch issue details') from e
|
||||
|
||||
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
|
||||
"""Get instructions for the conversation.
|
||||
@@ -512,7 +514,7 @@ class JiraFactory:
|
||||
except StartingConvoException:
|
||||
raise # Re-raise with original message
|
||||
except Exception as e:
|
||||
raise StartingConvoException(f'Failed to fetch issue details: {str(e)}')
|
||||
raise StartingConvoException('Failed to fetch issue details') from e
|
||||
|
||||
# Infer and select repository
|
||||
selected_repo = await JiraFactory._infer_repository(
|
||||
|
||||
@@ -436,9 +436,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
service_account = resolve_jira_dc_service_account(
|
||||
workspace, self.token_manager
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'[Jira DC] Service account configuration is invalid: {str(e)}'
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira DC] Service account configuration is invalid',
|
||||
stack_info=True,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -487,8 +488,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
service_account.api_key,
|
||||
bot_email=service_account.email,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'[Jira DC] Failed to get issue context: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('[Jira DC] Failed to get issue context', stack_info=True)
|
||||
await self._send_error_comment(
|
||||
job_context,
|
||||
'Failed to retrieve issue details. Please check the issue key and try again.',
|
||||
@@ -504,9 +505,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
jira_dc_user,
|
||||
workspace,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'[Jira DC] Failed to create jira dc view: {str(e)}', exc_info=True
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira DC] Failed to create jira dc view',
|
||||
stack_info=True,
|
||||
)
|
||||
await self._send_error_comment(
|
||||
job_context,
|
||||
@@ -566,8 +568,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'[Jira DC] Error in is_job_requested: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('[Jira DC] Error in is_job_requested', stack_info=True)
|
||||
return False
|
||||
|
||||
async def start_job(self, jira_dc_view: JiraDcViewInterface) -> None:
|
||||
@@ -608,9 +610,10 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
logger.warning(f'[Jira DC] User token unavailable: {str(e)}')
|
||||
msg_info = get_jira_dc_relink_message(jira_dc_view.job_context.display_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'[Jira DC] Unexpected error starting job: {str(e)}', exc_info=True
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira DC] Unexpected error starting job',
|
||||
stack_info=True,
|
||||
)
|
||||
msg_info = 'Sorry, there was an unexpected error starting the job. Please try again.'
|
||||
|
||||
@@ -625,8 +628,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
base_api_url=jira_dc_view.job_context.base_api_url,
|
||||
svc_acc_api_key=service_account.api_key,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'[Jira] Failed to send response message: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('[Jira] Failed to send response message', stack_info=True)
|
||||
|
||||
async def _resolve_service_account_mentions(self, payload: Dict) -> set[str] | None:
|
||||
"""Best-effort bot identifiers (username + Jira key) for a picker mention.
|
||||
@@ -1007,8 +1010,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
base_api_url=job_context.base_api_url,
|
||||
svc_acc_api_key=service_account.api_key,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'[Jira DC] Failed to send error comment: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('[Jira DC] Failed to send error comment', stack_info=True)
|
||||
|
||||
async def _send_repo_selection_comment(
|
||||
self,
|
||||
@@ -1055,7 +1058,8 @@ class JiraDcManager(Manager[JiraDcViewInterface]):
|
||||
f'[Jira] Sent repository selection comment for issue {jira_dc_view.job_context.issue_key}'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'[Jira] Failed to send repository selection comment: {str(e)}'
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'[Jira] Failed to send repository selection comment',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor):
|
||||
detail=summary,
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.exception(f'[Jira DC] Failed to post summary: {e}', stack_info=True)
|
||||
_logger.exception('[Jira DC] Failed to post summary', stack_info=True)
|
||||
return EventCallbackResult(
|
||||
status=EventCallbackResultStatus.ERROR,
|
||||
event_callback_id=callback.id,
|
||||
@@ -209,7 +209,9 @@ class JiraDcV1CallbackProcessor(EventCallbackProcessor):
|
||||
dict(e.response.headers),
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to agent server: {error_detail}')
|
||||
raise Exception(
|
||||
f'Failed to send message to agent server: {error_detail}'
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_detail = (
|
||||
|
||||
@@ -334,12 +334,13 @@ class JiraDcExistingConversationView(JiraDcViewInterface):
|
||||
f'[Jira DC] Sent message to existing conversation {self.conversation_id}'
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
f'[Jira DC] Failed to send message: HTTP {e.response.status_code}'
|
||||
logger.exception(
|
||||
f'[Jira DC] Failed to send message: HTTP {e.response.status_code}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'[Jira DC] Failed to send message: {e}')
|
||||
except Exception:
|
||||
logger.exception('[Jira DC] Failed to send message', stack_info=True)
|
||||
raise
|
||||
|
||||
def get_response_msg(self) -> str:
|
||||
|
||||
@@ -70,9 +70,9 @@ async def resolve_org_for_repo(
|
||||
f'for {provider}/{git_org} (user {keycloak_user_id})',
|
||||
)
|
||||
return claim.org_id
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'[OrgResolver] Error resolving org for {provider}/{git_org}: {e}',
|
||||
exc_info=True,
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'[OrgResolver] Error resolving org for {provider}/{git_org}',
|
||||
stack_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -137,19 +137,19 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'slack_store_user_msg_failed',
|
||||
extra={
|
||||
'message_ts': message_ts,
|
||||
'thread_ts': thread_ts,
|
||||
'key': key,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise SlackError(
|
||||
SlackErrorCode.REDIS_STORE_FAILED,
|
||||
log_context={'message_ts': message_ts, 'thread_ts': thread_ts},
|
||||
)
|
||||
) from e
|
||||
|
||||
async def _retrieve_user_msg_for_form(
|
||||
self, message_ts: str, thread_ts: str | None
|
||||
@@ -200,19 +200,19 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
except SlackError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'slack_retrieve_user_msg_failed',
|
||||
extra={
|
||||
'message_ts': message_ts,
|
||||
'thread_ts': thread_ts,
|
||||
'key': key,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise SlackError(
|
||||
SlackErrorCode.REDIS_RETRIEVE_FAILED,
|
||||
log_context={'message_ts': message_ts, 'thread_ts': thread_ts},
|
||||
)
|
||||
) from e
|
||||
|
||||
async def _claim_form_interaction(
|
||||
self, team_id: str, channel_id: str, message_ts: str, thread_ts: str | None
|
||||
@@ -256,19 +256,19 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'slack_claim_form_interaction_failed',
|
||||
extra={
|
||||
'message_ts': message_ts,
|
||||
'thread_ts': thread_ts,
|
||||
'key': key,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise SlackError(
|
||||
SlackErrorCode.REDIS_STORE_FAILED,
|
||||
log_context={'message_ts': message_ts, 'thread_ts': thread_ts},
|
||||
)
|
||||
) from e
|
||||
|
||||
async def _replace_repo_selection_form(
|
||||
self, response_url: str | None, selected_repository: str | None
|
||||
@@ -464,10 +464,9 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
except SlackError as e:
|
||||
await self.handle_slack_error(message.message, e)
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'slack_unexpected_error',
|
||||
extra={'error': str(e), **message.message},
|
||||
'slack_unexpected_error', extra={**message.message}, stack_info=True
|
||||
)
|
||||
await self.handle_slack_error(
|
||||
message.message,
|
||||
@@ -566,10 +565,9 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
except SlackError as e:
|
||||
await self.handle_slack_error(payload, e)
|
||||
return
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'slack_unexpected_error',
|
||||
extra={'error': str(e), **payload},
|
||||
'slack_unexpected_error', extra={**payload}, stack_info=True
|
||||
)
|
||||
await self.handle_slack_error(
|
||||
payload, SlackError(SlackErrorCode.UNEXPECTED_ERROR)
|
||||
@@ -861,7 +859,7 @@ class SlackManager(Manager[SlackViewInterface]):
|
||||
await self.send_message(msg_info, slack_view)
|
||||
|
||||
except Exception:
|
||||
logger.exception('[Slack]: Error starting job')
|
||||
logger.exception('[Slack]: Error starting job', stack_info=True)
|
||||
await self.send_message(
|
||||
'Uh oh! There was an unexpected error starting the job :(', slack_view
|
||||
)
|
||||
|
||||
@@ -138,8 +138,10 @@ class SlackV1CallbackProcessor(EventCallbackProcessor):
|
||||
channel_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error('[Slack V1] Failed to post message to Slack: %s', e)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'[Slack V1] Failed to post message to Slack', stack_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -186,27 +188,27 @@ class SlackV1CallbackProcessor(EventCallbackProcessor):
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'[Slack V1] HTTP error fetching final response from %s: %s. '
|
||||
'Response headers: %s',
|
||||
url,
|
||||
error_detail,
|
||||
dict(e.response.headers),
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(
|
||||
f'Failed to fetch final response from agent server: {error_detail}'
|
||||
)
|
||||
) from e
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_detail = f'Request timeout after 30 seconds to {url}'
|
||||
_logger.error('[Slack V1] %s', error_detail, exc_info=True)
|
||||
_logger.exception('[Slack V1] %s', error_detail, stack_info=True)
|
||||
raise Exception(error_detail)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
error_detail = f'Request error to {url}: {str(e)}'
|
||||
_logger.error('[Slack V1] %s', error_detail, exc_info=True)
|
||||
raise Exception(error_detail)
|
||||
_logger.exception('[Slack V1] %s', error_detail, stack_info=True)
|
||||
raise Exception(error_detail) from e
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Final response orchestration
|
||||
|
||||
@@ -440,13 +440,13 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
|
||||
response.raise_for_status()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'[Slack V1] Failed to send message to conversation %s: %s',
|
||||
self.conversation_id,
|
||||
str(e),
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
raise Exception(f'Failed to send message to v1 conversation: {str(e)}')
|
||||
raise Exception('Failed to send message to v1 conversation') from e
|
||||
|
||||
async def create_or_update_conversation(self, jinja: Environment) -> str:
|
||||
"""Send new user message to conversation."""
|
||||
|
||||
@@ -41,7 +41,7 @@ class SaasAppLifespanService(AppLifespanService):
|
||||
if svc is not None:
|
||||
svc.shutdown()
|
||||
except Exception:
|
||||
logger.exception('Error shutting down analytics service')
|
||||
logger.exception('Error shutting down analytics service', stack_info=True)
|
||||
|
||||
# Release long-lived database resources: the GCP Cloud SQL connector
|
||||
# (background cert-refresh tasks + aiohttp ClientSession) and the
|
||||
@@ -52,4 +52,4 @@ class SaasAppLifespanService(AppLifespanService):
|
||||
|
||||
await get_global_config().db_session.close()
|
||||
except Exception:
|
||||
logger.exception('Error closing DB session injector')
|
||||
logger.exception('Error closing DB session injector', stack_info=True)
|
||||
|
||||
@@ -153,9 +153,10 @@ class SaasUserAuth(UserAuth):
|
||||
try:
|
||||
user_uuid = UUID(self.user_id)
|
||||
except ValueError as exc:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'effective_org_id_override_invalid_user_id',
|
||||
extra={'user_id': self.user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -266,9 +267,10 @@ class SaasUserAuth(UserAuth):
|
||||
user_uuid = UUID(self.user_id)
|
||||
except ValueError as exc:
|
||||
# Shouldn't happen, but treat as not-a-member.
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'x_org_id_invalid_user_id',
|
||||
extra={'user_id': self.user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -564,14 +566,15 @@ class SaasUserAuth(UserAuth):
|
||||
provider_tokens[idp_type] = ProviderToken(
|
||||
token=SecretStr(provider_token), user_id=None, host=host
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# If there was a problem with a refresh token we log and delete it
|
||||
logger.error(
|
||||
f'Error refreshing provider_token token: {e}',
|
||||
logger.exception(
|
||||
'Error refreshing provider_token token',
|
||||
extra={
|
||||
'user_id': self.user_id,
|
||||
'idp_type': token.identity_provider,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
async with a_session_maker() as session:
|
||||
await session.execute(
|
||||
@@ -713,8 +716,10 @@ class SaasUserAuth(UserAuth):
|
||||
except HTTPException:
|
||||
# Propagate validation errors raised by get_effective_org_id().
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching org info for user {self.user_id}: {e}')
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Error fetching org info for user {self.user_id}', stack_info=True
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -24,7 +24,9 @@ class GoogleSheetsClient:
|
||||
self.client = gspread.authorize(credentials)
|
||||
logger.info('Successfully initialized Google Sheets API service')
|
||||
except Exception:
|
||||
logger.exception('Failed to initialize Google Sheets client')
|
||||
logger.exception(
|
||||
'Failed to initialize Google Sheets client', stack_info=True
|
||||
)
|
||||
self.client = None
|
||||
|
||||
def _get_from_cache(
|
||||
@@ -102,10 +104,13 @@ class GoogleSheetsClient:
|
||||
return usernames
|
||||
|
||||
except gspread.exceptions.APIError:
|
||||
logger.exception(f'Error accessing Google Sheet {spreadsheet_id}')
|
||||
logger.exception(
|
||||
f'Error accessing Google Sheet {spreadsheet_id}', stack_info=True
|
||||
)
|
||||
return []
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Unexpected error accessing Google Sheet {spreadsheet_id}'
|
||||
f'Unexpected error accessing Google Sheet {spreadsheet_id}',
|
||||
stack_info=True,
|
||||
)
|
||||
return []
|
||||
|
||||
@@ -130,7 +130,7 @@ class TokenManager:
|
||||
|
||||
return token_response['access_token'], token_response['refresh_token']
|
||||
except Exception:
|
||||
logger.exception('Exception when getting Keycloak tokens')
|
||||
logger.exception('Exception when getting Keycloak tokens', stack_info=True)
|
||||
return None, None
|
||||
|
||||
async def verify_keycloak_token(
|
||||
@@ -313,10 +313,11 @@ class TokenManager:
|
||||
return access_token
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Log the full response details including the body
|
||||
logger.error(
|
||||
logger.exception(
|
||||
f'Failed to get tokens for user {user_id}, identity provider {idp} from URL {e.response.url}. '
|
||||
f'Status code: {e.response.status_code}, '
|
||||
f'Response body: {e.response.text}'
|
||||
f'Response body: {e.response.text}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise ValueError(
|
||||
f'Failed to get token for user: {user_id}, identity provider: {idp}. '
|
||||
@@ -571,7 +572,9 @@ class TokenManager:
|
||||
)
|
||||
return await self.get_idp_token(tokens['access_token'], idp)
|
||||
except KeycloakConnectionError:
|
||||
logger.exception('KeycloakConnectionError when refreshing token')
|
||||
logger.exception(
|
||||
'KeycloakConnectionError when refreshing token', stack_info=True
|
||||
)
|
||||
raise
|
||||
except KeycloakPostError as e:
|
||||
error_message = str(e)
|
||||
@@ -603,9 +606,10 @@ class TokenManager:
|
||||
return await self.get_idp_token_from_offline_token(
|
||||
offline_token=offline_token, idp=idp
|
||||
)
|
||||
except KeycloakConnectionError as e:
|
||||
except KeycloakConnectionError:
|
||||
logger.exception(
|
||||
f'KeycloakConnectionError when getting IDP token for IDP user_id {idp_user_id}: {str(e)}'
|
||||
f'KeycloakConnectionError when getting IDP token for IDP user_id {idp_user_id}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -776,10 +780,14 @@ class TokenManager:
|
||||
return self._find_duplicate_in_users(users, base_email, current_user_id)
|
||||
|
||||
except KeycloakConnectionError:
|
||||
logger.exception('KeycloakConnectionError when checking duplicate email')
|
||||
logger.exception(
|
||||
'KeycloakConnectionError when checking duplicate email', stack_info=True
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Unexpected error checking duplicate email: {e}')
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error checking duplicate email', stack_info=True
|
||||
)
|
||||
# On any error, allow signup to proceed (fail open)
|
||||
return False
|
||||
|
||||
@@ -808,7 +816,9 @@ class TokenManager:
|
||||
logger.info(f'Successfully deleted Keycloak user {user_id}')
|
||||
return True
|
||||
except KeycloakConnectionError:
|
||||
logger.exception(f'KeycloakConnectionError when deleting user {user_id}')
|
||||
logger.exception(
|
||||
f'KeycloakConnectionError when deleting user {user_id}', stack_info=True
|
||||
)
|
||||
raise
|
||||
except KeycloakError as e:
|
||||
# User might not exist or already deleted
|
||||
@@ -817,8 +827,11 @@ class TokenManager:
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.exception(f'Unexpected error deleting Keycloak user {user_id}: {e}')
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Unexpected error deleting Keycloak user {user_id}',
|
||||
stack_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
@retry(
|
||||
@@ -966,12 +979,12 @@ class TokenManager:
|
||||
logger.warning(
|
||||
f'User not found in Keycloak when attempting to disable: {user_id}'
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# Log error but don't raise - the caller should handle the blocking regardless
|
||||
email_str = f', email: {email}' if email else ''
|
||||
logger.error(
|
||||
f'Failed to disable Keycloak account for user_id: {user_id}{email_str}: {str(e)}',
|
||||
exc_info=True,
|
||||
logger.exception(
|
||||
f'Failed to disable Keycloak account for user_id: {user_id}{email_str}',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
async def store_org_token(self, installation_id: int, installation_token: str):
|
||||
@@ -1112,5 +1125,5 @@ class TokenManager:
|
||||
refresh_token=refresh_token
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Exception when logging out of keycloak')
|
||||
logger.exception('Exception when logging out of keycloak', stack_info=True)
|
||||
raise
|
||||
|
||||
@@ -80,7 +80,9 @@ class DefaultUserAuthorizer(UserAuthorizer):
|
||||
|
||||
return UserAuthorizationResponse(success=True)
|
||||
except Exception:
|
||||
logger.exception('error authorizing user', extra={'user_id': user_id})
|
||||
logger.exception(
|
||||
'error authorizing user', extra={'user_id': user_id}, stack_info=True
|
||||
)
|
||||
return UserAuthorizationResponse(success=False)
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ class OrgBudgetMaintenanceProcessor(MaintenanceTaskProcessor):
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
'org_budget_maintenance_failed',
|
||||
extra={'org_id': org_id, 'error': str(exc)},
|
||||
extra={
|
||||
'org_id': org_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
errors.append({'org_id': org_id, 'error': str(exc)})
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ class SetAuthCookieMiddleware:
|
||||
raise AuthError('Invalid authentication token')
|
||||
except Exception as e:
|
||||
logger.warning(f'JWT decode error: {str(e)}')
|
||||
raise AuthError('Invalid authentication token')
|
||||
raise AuthError('Invalid authentication token') from e
|
||||
else:
|
||||
# Don't fail an API call if the TOS has not been accepted.
|
||||
# The user will accept the TOS the next time they login.
|
||||
|
||||
@@ -65,14 +65,17 @@ class RateLimiter:
|
||||
try:
|
||||
allowed = await self.strategy.hit(lim, namespace, key)
|
||||
except Exception:
|
||||
logger.exception('Rate limit check could not complete, redis issue?')
|
||||
logger.exception(
|
||||
'Rate limit check could not complete, redis issue?', stack_info=True
|
||||
)
|
||||
if not allowed:
|
||||
logger.info(f'Rate limit hit for {namespace}:{key}')
|
||||
try:
|
||||
result = await self._get_stats_as_result(lim, namespace, key)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Rate limit exceeded but window lookup failed, swallowing'
|
||||
'Rate limit exceeded but window lookup failed, swallowing',
|
||||
stack_info=True,
|
||||
)
|
||||
else:
|
||||
raise RateLimitException(result)
|
||||
|
||||
@@ -126,7 +126,7 @@ async def _get_org(org_id: UUID, user_id: str) -> Org:
|
||||
try:
|
||||
return await OrgService.get_org_by_id(org_id=org_id, user_id=user_id)
|
||||
except OrgNotFoundError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
||||
|
||||
|
||||
async def _get_member(
|
||||
@@ -277,7 +277,7 @@ async def save_agent_profile(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=safe_validation_error_detail(e),
|
||||
)
|
||||
) from e
|
||||
except Exception:
|
||||
# SkillValidationError / schema errors are client errors, never a
|
||||
# 500; stay generic — these messages can embed the input.
|
||||
|
||||
@@ -105,6 +105,7 @@ async def track_frontend_event(
|
||||
logger.exception(
|
||||
'analytics:frontend_event:failed',
|
||||
extra={'event_type': body.event_type},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
return AnalyticsEventResponse(status='ok')
|
||||
|
||||
@@ -82,10 +82,13 @@ async def generate_byor_key(user_id: str, org_id: UUID) -> str | None:
|
||||
},
|
||||
)
|
||||
return key
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error generating BYOR key',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -106,10 +109,13 @@ async def delete_byor_key_from_litellm(
|
||||
extra={'user_id': user_id},
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error deleting BYOR key from LiteLLM',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -216,13 +222,11 @@ async def check_byor_permitted(
|
||||
)
|
||||
return ByorPermittedResponse(permitted=permitted)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Error checking BYOR export permission', extra={'error': str(e)}
|
||||
)
|
||||
logger.exception('Error checking BYOR export permission', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to check BYOR export permission',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@api_router.post('', tags=['Keys'])
|
||||
@@ -310,7 +314,7 @@ async def create_api_key(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception('Error creating API key')
|
||||
logger.exception('Error creating API key', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create API key',
|
||||
@@ -327,7 +331,7 @@ async def list_api_keys(
|
||||
keys = await api_key_store.list_api_keys(user_id, org_id=effective_org_id)
|
||||
return [api_key_to_response(key) for key in keys]
|
||||
except Exception:
|
||||
logger.exception('Error listing API keys')
|
||||
logger.exception('Error listing API keys', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to list API keys',
|
||||
@@ -369,7 +373,7 @@ async def delete_api_key(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception('Error deleting API key')
|
||||
logger.exception('Error deleting API key', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to delete API key',
|
||||
@@ -492,11 +496,11 @@ async def get_llm_api_key_for_byor(
|
||||
# Re-raise HTTP exceptions as-is
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception('Error retrieving BYOR LLM API key', extra={'error': str(e)})
|
||||
logger.exception('Error retrieving BYOR LLM API key', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve BYOR LLM API key',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@api_router.post('/llm/byor/refresh', tags=['Keys'])
|
||||
@@ -560,7 +564,7 @@ async def refresh_llm_api_key_for_byor(
|
||||
)
|
||||
return LlmApiKeyResponse(key=key)
|
||||
except HTTPException as he:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'HTTP exception during BYOR LLM API key refresh',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
@@ -568,6 +572,7 @@ async def refresh_llm_api_key_for_byor(
|
||||
'detail': he.detail,
|
||||
'exception_type': type(he).__name__,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -575,11 +580,11 @@ async def refresh_llm_api_key_for_byor(
|
||||
'Unexpected error refreshing BYOR LLM API key',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
'exception_type': type(e).__name__,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to refresh BYOR LLM API key',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -178,6 +178,7 @@ async def _get_user_orgs_with_data(user_id: str, org_member_ids: list) -> list:
|
||||
logger.exception(
|
||||
'auth:_get_user_orgs_with_data:failed',
|
||||
extra={'user_id': user_id, 'org_ids': [str(oid) for oid in org_member_ids]},
|
||||
stack_info=True,
|
||||
)
|
||||
return []
|
||||
|
||||
@@ -217,6 +218,7 @@ async def _track_login_analytics_background(
|
||||
logger.exception(
|
||||
'auth:identify_user:member_count_failed',
|
||||
extra={'user_id': user_id, 'org_id': str(org.id)},
|
||||
stack_info=True,
|
||||
)
|
||||
member_count = None
|
||||
orgs_data.append(
|
||||
@@ -248,6 +250,7 @@ async def _track_login_analytics_background(
|
||||
logger.exception(
|
||||
'auth:_track_login_analytics_background:failed',
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -393,8 +396,10 @@ async def keycloak_callback(
|
||||
error_url = f'{web_url}/login?recaptcha_blocked=true'
|
||||
return RedirectResponse(error_url, status_code=302)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'reCAPTCHA verification error at callback: {e}')
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'reCAPTCHA verification error at callback', stack_info=True
|
||||
)
|
||||
# Fail open - continue with login if reCAPTCHA service unavailable
|
||||
|
||||
# Check email verification status
|
||||
@@ -571,10 +576,13 @@ async def keycloak_callback(
|
||||
else:
|
||||
redirect_url = f'{redirect_url}?email_mismatch=true'
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error processing invitation during auth callback',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
# Don't fail the login if invitation processing fails
|
||||
if '?' in redirect_url:
|
||||
@@ -591,10 +599,13 @@ async def keycloak_callback(
|
||||
)
|
||||
if accepted_invitations:
|
||||
user = await UserStore.get_user_by_id(user_id) or user
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error accepting pending invitations at login',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -602,10 +613,13 @@ async def keycloak_callback(
|
||||
user,
|
||||
is_new_user=is_new_user,
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error applying default organization bootstrap',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
# If the user hasn't accepted the TOS, redirect to the TOS page
|
||||
@@ -1021,7 +1035,7 @@ async def accept_tos(request: Request):
|
||||
properties={'signed_up_at': datetime.now(timezone.utc).isoformat()},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:user_signed_up:failed')
|
||||
logger.exception('analytics:user_signed_up:failed', stack_info=True)
|
||||
|
||||
# Determine final redirect - but don't override if it's the offline token flow
|
||||
# (the offline callback will handle post-auth redirect after storing the token)
|
||||
@@ -1159,7 +1173,7 @@ async def complete_onboarding(
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:onboarding_completed:failed')
|
||||
logger.exception('analytics:onboarding_completed:failed', stack_info=True)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
|
||||
@@ -331,7 +331,7 @@ async def success_callback(session_id: str, request: Request):
|
||||
credit_balance_after=new_max_budget,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:credit_purchased:failed')
|
||||
logger.exception('analytics:credit_purchased:failed', stack_info=True)
|
||||
|
||||
return RedirectResponse(
|
||||
f'{get_web_url(request)}/settings/billing?checkout=success', status_code=302
|
||||
|
||||
@@ -104,13 +104,15 @@ async def update_email(
|
||||
|
||||
except ValueError as e:
|
||||
# Handle validation errors from Pydantic
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.exception(f'Error updating email: {str(e)}')
|
||||
logger.exception('Error updating email', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='An error occurred while updating the email',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@api_router.put('/resend')
|
||||
|
||||
@@ -235,11 +235,11 @@ async def get_azure_devops_resources(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error retrieving Azure DevOps resources: {e}')
|
||||
logger.exception('Error retrieving Azure DevOps resources', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve Azure DevOps resources',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@azure_devops_integration_router.post('/azure-devops/reinstall-webhook')
|
||||
@@ -299,11 +299,13 @@ async def reinstall_azure_devops_webhook(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error installing Azure DevOps resolver hooks: {e}')
|
||||
logger.exception(
|
||||
'Error installing Azure DevOps resolver hooks', stack_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to install Azure DevOps resolver hooks',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@azure_devops_integration_router.post('/azure-devops/uninstall-webhook')
|
||||
@@ -355,11 +357,13 @@ async def uninstall_azure_devops_webhook(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error uninstalling Azure DevOps resolver hooks: {e}')
|
||||
logger.exception(
|
||||
'Error uninstalling Azure DevOps resolver hooks', stack_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to uninstall Azure DevOps resolver hooks',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@azure_devops_integration_router.post('/azure-devops/events')
|
||||
@@ -413,6 +417,6 @@ async def azure_devops_events(
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing Azure DevOps event: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing Azure DevOps event', stack_info=True)
|
||||
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
|
||||
|
||||
@@ -121,6 +121,6 @@ async def bitbucket_events(
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing Bitbucket event: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing Bitbucket event', stack_info=True)
|
||||
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
|
||||
|
||||
@@ -347,11 +347,11 @@ async def get_bitbucket_dc_resources(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error retrieving Bitbucket DC resources: {e}')
|
||||
logger.exception('Error retrieving Bitbucket DC resources', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve Bitbucket DC resources',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@bitbucket_dc_integration_router.post('/bitbucket-dc/enroll-webhook')
|
||||
@@ -392,11 +392,11 @@ async def enroll_bitbucket_dc_webhook(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'Error enrolling Bitbucket DC webhook: {e}')
|
||||
logger.exception('Error enrolling Bitbucket DC webhook', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to enroll Bitbucket DC webhook',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@bitbucket_dc_integration_router.patch('/bitbucket-dc/webhook-id')
|
||||
@@ -445,11 +445,11 @@ async def update_bitbucket_dc_webhook_id(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error updating Bitbucket DC webhook id: {e}')
|
||||
logger.exception('Error updating Bitbucket DC webhook id', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to update Bitbucket DC webhook id',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@bitbucket_dc_integration_router.post('/bitbucket-dc/reinstall-webhook')
|
||||
@@ -526,11 +526,11 @@ async def reinstall_bitbucket_dc_webhook(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error installing Bitbucket DC webhook: {e}')
|
||||
logger.exception('Error installing Bitbucket DC webhook', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to install Bitbucket DC webhook',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@bitbucket_dc_integration_router.post('/bitbucket-dc/uninstall-webhook')
|
||||
@@ -603,11 +603,11 @@ async def uninstall_bitbucket_dc_webhook(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error uninstalling Bitbucket DC webhook: {e}')
|
||||
logger.exception('Error uninstalling Bitbucket DC webhook', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to uninstall Bitbucket DC webhook',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
async def _handle_bitbucket_dc_event(
|
||||
@@ -711,7 +711,7 @@ async def _handle_bitbucket_dc_event(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing Bitbucket DC event: {e}')
|
||||
logger.exception('Error processing Bitbucket DC event', stack_info=True)
|
||||
# Surface the exception class name so admins reading DC's webhook
|
||||
# delivery UI can correlate with server logs without leaking a full
|
||||
# message (which may contain sensitive payload fragments).
|
||||
|
||||
@@ -102,6 +102,6 @@ async def github_events(
|
||||
status_code=408,
|
||||
content={'error': 'Request timeout - client took too long to send data.'},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing GitHub event: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing GitHub event', stack_info=True)
|
||||
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
|
||||
|
||||
@@ -127,8 +127,8 @@ async def gitlab_events(
|
||||
content={'message': 'GitLab events endpoint reached successfully.'},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing GitLab event: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing GitLab event', stack_info=True)
|
||||
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
|
||||
|
||||
|
||||
@@ -259,11 +259,11 @@ async def get_gitlab_resources(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error retrieving GitLab resources: {e}')
|
||||
logger.exception('Error retrieving GitLab resources', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve GitLab resources',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@gitlab_integration_router.post('/gitlab/reinstall-webhook')
|
||||
@@ -388,8 +388,8 @@ async def reinstall_gitlab_webhook(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error reinstalling GitLab webhook: {e}')
|
||||
logger.exception('Error reinstalling GitLab webhook', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to reinstall webhook',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -327,8 +327,8 @@ async def jira_events(
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (like signature verification failures)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing Jira webhook: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing Jira webhook', stack_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={'error': 'Internal server error processing webhook.'},
|
||||
@@ -392,11 +392,11 @@ async def create_jira_workspace(request: Request, workspace_data: JiraWorkspaceC
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error creating Jira workspace: {e}')
|
||||
logger.exception('Error creating Jira workspace', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create workspace',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_integration_router.post('/workspaces/link')
|
||||
@@ -451,11 +451,11 @@ async def create_workspace_link(request: Request, link_data: JiraLinkCreate):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error registering Jira user: {e}')
|
||||
logger.exception('Error registering Jira user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to register user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_integration_router.get('/callback')
|
||||
@@ -655,11 +655,11 @@ async def get_current_workspace_link(request: Request):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error retrieving Jira user: {e}')
|
||||
logger.exception('Error retrieving Jira user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_integration_router.post('/workspaces/unlink')
|
||||
@@ -707,11 +707,11 @@ async def unlink_workspace(request: Request):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error unlinking Jira user: {e}')
|
||||
logger.exception('Error unlinking Jira user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to unlink user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_integration_router.get(
|
||||
@@ -762,8 +762,8 @@ async def validate_workspace_integration(request: Request, workspace_name: str):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error validating Jira organization: {e}')
|
||||
logger.exception('Error validating Jira organization', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to validate organization',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -463,8 +463,8 @@ async def _process_jira_dc_event(
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (like signature verification failures)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing Jira DC webhook: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error processing Jira DC webhook', stack_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={'error': 'Internal server error processing webhook.'},
|
||||
@@ -784,11 +784,11 @@ async def create_jira_dc_workspace(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error creating Jira DC workspace: {e}')
|
||||
logger.exception('Error creating Jira DC workspace', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create workspace',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_dc_integration_router.post('/workspaces/status')
|
||||
@@ -820,11 +820,11 @@ async def update_jira_dc_workspace_status(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error updating Jira DC workspace status: {e}')
|
||||
logger.exception('Error updating Jira DC workspace status', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to update workspace status',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_dc_integration_router.post('/workspaces/link')
|
||||
@@ -899,11 +899,11 @@ async def create_workspace_link(request: Request, link_data: JiraDcLinkCreate):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error registering Jira DC user: {e}')
|
||||
logger.exception('Error registering Jira DC user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to register user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_dc_integration_router.get('/callback')
|
||||
@@ -1218,11 +1218,11 @@ async def get_current_workspace_link(request: Request):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error retrieving Jira DC user: {e}')
|
||||
logger.exception('Error retrieving Jira DC user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_dc_integration_router.post('/workspaces/unlink')
|
||||
@@ -1297,11 +1297,11 @@ async def unlink_workspace(request: Request):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error unlinking Jira DC user: {e}')
|
||||
logger.exception('Error unlinking Jira DC user', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to unlink user',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@jira_dc_integration_router.get(
|
||||
@@ -1346,8 +1346,8 @@ async def validate_workspace_integration(request: Request, workspace_name: str):
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f'Error validating Jira DC workspace: {e}')
|
||||
logger.exception('Error validating Jira DC workspace', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to validate workspace',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -137,7 +137,7 @@ async def install_callback(
|
||||
|
||||
return RedirectResponse(auth_url)
|
||||
except Exception: # type: ignore
|
||||
logger.error('unexpected_error', exc_info=True, stack_info=True)
|
||||
logger.exception('unexpected_error', stack_info=True)
|
||||
return _html_response(
|
||||
title='Error',
|
||||
description='Internal server Error',
|
||||
@@ -442,8 +442,8 @@ async def on_options_load(request: Request, background_tasks: BackgroundTasks):
|
||||
extra={
|
||||
'slack_user_id': slack_user_id,
|
||||
'search_value': search_value,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
# Notify user about the unexpected error with error code
|
||||
background_tasks.add_task(
|
||||
|
||||
@@ -115,7 +115,7 @@ async def device_authorization(
|
||||
interval=device_code_entry.current_interval,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception('Error in device authorization: %s', str(e))
|
||||
logger.exception('Error in device authorization: %s', str(e), stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Internal server error',
|
||||
@@ -231,7 +231,7 @@ async def device_token(device_code: str = Form(...)):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception('Error in device token: %s', str(e))
|
||||
logger.exception('Error in device token: %s', str(e), stack_info=True)
|
||||
return _oauth_error(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
'server_error',
|
||||
@@ -298,7 +298,9 @@ async def device_verification_authenticated(
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Failed to create device API key after authorization: %s', str(e)
|
||||
'Failed to create device API key after authorization: %s',
|
||||
str(e),
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
# Clean up: revert the device authorization since API key creation failed
|
||||
@@ -313,12 +315,13 @@ async def device_verification_authenticated(
|
||||
logger.exception(
|
||||
'Failed to revert device authorization during cleanup: %s',
|
||||
str(cleanup_error),
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create API key for device access.',
|
||||
)
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
'Device code authorized with API key successfully',
|
||||
@@ -354,6 +357,7 @@ async def device_verification_authenticated(
|
||||
logger.exception(
|
||||
'oauth_device:analytics:failed',
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
@@ -364,8 +368,8 @@ async def device_verification_authenticated(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception('Error in device verification: %s', str(e))
|
||||
logger.exception('Error in device verification: %s', str(e), stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='An unexpected error occurred. Please try again.',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -139,7 +139,7 @@ async def create_invitation(
|
||||
role=invitation_data.role,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:team_members_invited:failed')
|
||||
logger.exception('analytics:team_members_invited:failed', stack_info=True)
|
||||
|
||||
successful_responses = [
|
||||
await InvitationResponse.from_invitation(inv) for inv in successful
|
||||
@@ -156,21 +156,24 @@ async def create_invitation(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Unexpected error creating batch invitations',
|
||||
extra={'org_id': str(org_id), 'error': str(e)},
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='An unexpected error occurred',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@invitation_router.get(
|
||||
@@ -203,6 +206,7 @@ async def list_pending_invitations(
|
||||
logger.exception(
|
||||
'Error listing pending invitations',
|
||||
extra={'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -240,11 +244,12 @@ async def revoke_invitation(
|
||||
try:
|
||||
revoked = await OrgInvitationService.revoke_invitation(org_id, invitation_id)
|
||||
except InvitationInvalidError as e:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e)) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error revoking invitation',
|
||||
extra={'org_id': str(org_id), 'invitation_id': invitation_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -359,7 +364,7 @@ async def accept_invitation(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='invitation_invalid',
|
||||
)
|
||||
) from e
|
||||
|
||||
except UserAlreadyMemberError:
|
||||
logger.info(
|
||||
@@ -383,7 +388,7 @@ async def accept_invitation(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='email_mismatch',
|
||||
)
|
||||
) from e
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
@@ -391,10 +396,10 @@ async def accept_invitation(
|
||||
extra={
|
||||
'token_prefix': token[:10] + '...',
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='An unexpected error occurred',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -145,7 +145,7 @@ async def _get_org(org_id: UUID, user_id: str) -> Org:
|
||||
try:
|
||||
return await OrgService.get_org_by_id(org_id=org_id, user_id=user_id)
|
||||
except OrgNotFoundError as e:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
@@ -258,7 +258,9 @@ async def save_profile(
|
||||
try:
|
||||
profiles.save(name, llm, include_secrets=request.include_secrets)
|
||||
except ProfileLimitExceededError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
return ProfileMutationResponse(name=name, message=f"Profile '{name}' saved")
|
||||
|
||||
@@ -290,7 +292,9 @@ async def delete_profile(
|
||||
try:
|
||||
delete_llm_profile(agent_profiles, OrgLLMProfileMutator(profiles), name)
|
||||
except ProfileReferenced as exc:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
return ProfileMutationResponse(name=name, message=f"Profile '{name}' deleted")
|
||||
|
||||
@@ -421,14 +425,18 @@ async def rename_profile(
|
||||
request.new_name,
|
||||
)
|
||||
except ProfileNotFoundError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
||||
) from exc
|
||||
except ProfileAlreadyExistsError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
||||
) from exc
|
||||
except ValueError as exc:
|
||||
# rename_llm_profile validates new_name against PROFILE_NAME_PATTERN.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
|
||||
)
|
||||
) from exc
|
||||
# The cascade may have repointed agent-profile refs — persist them too.
|
||||
after = agent_profiles.model_dump(mode='json', context={'expose_secrets': True})
|
||||
if after != before:
|
||||
|
||||
@@ -171,10 +171,11 @@ async def list_user_orgs(
|
||||
current_org_id=current_org_id,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error listing organizations',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -235,29 +236,32 @@ async def create_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
)
|
||||
except LiteLLMIntegrationError as e:
|
||||
logger.error(
|
||||
) from e
|
||||
except LiteLLMIntegrationError:
|
||||
logger.exception(
|
||||
'LiteLLM integration failed',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create LiteLLM integration',
|
||||
)
|
||||
except OrgDatabaseError as e:
|
||||
logger.error(
|
||||
except OrgDatabaseError:
|
||||
logger.exception(
|
||||
'Database operation failed',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create organization',
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error creating organization',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -281,11 +285,12 @@ async def get_org_defaults_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error getting organization defaults settings',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -330,11 +335,12 @@ async def update_org_defaults_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
except OrgDatabaseError as e:
|
||||
logger.error(
|
||||
) from e
|
||||
except OrgDatabaseError:
|
||||
logger.exception(
|
||||
'Database error updating organization defaults settings',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -342,10 +348,11 @@ async def update_org_defaults_settings(
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error updating organization defaults settings',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -373,13 +380,14 @@ async def get_legacy_org_defaults_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error getting legacy organization defaults settings',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -413,13 +421,14 @@ async def update_legacy_org_defaults_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error updating legacy organization defaults settings',
|
||||
extra={'user_id': user_id, 'error': str(e)},
|
||||
extra={'user_id': user_id},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -459,10 +468,10 @@ async def get_org_app_settings(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Current organization not found',
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error retrieving organization app settings',
|
||||
extra={'error': str(e)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -514,7 +523,7 @@ async def update_org_app_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except OrgNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -524,13 +533,13 @@ async def update_org_app_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error updating organization app settings',
|
||||
extra={'error': str(e)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -568,11 +577,12 @@ async def get_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error retrieving organization',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -626,15 +636,17 @@ async def get_me(
|
||||
'org_id': str(org_id),
|
||||
'role_id': e.role_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='An unexpected error occurred',
|
||||
)
|
||||
except Exception as e:
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error retrieving member details',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -711,7 +723,7 @@ async def delete_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except OrgAuthorizationError as e:
|
||||
logger.warning(
|
||||
'User not authorized to delete organization',
|
||||
@@ -720,7 +732,7 @@ async def delete_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except OrphanedUserError as e:
|
||||
logger.warning(
|
||||
'Cannot delete organization: other members would be orphaned',
|
||||
@@ -733,20 +745,22 @@ async def delete_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
except OrgDatabaseError as e:
|
||||
logger.error(
|
||||
) from e
|
||||
except OrgDatabaseError:
|
||||
logger.exception(
|
||||
'Database error during organization deletion',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to delete organization',
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error during organization deletion',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -810,31 +824,33 @@ async def update_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except OrgNameExistsError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except PermissionError as e:
|
||||
# User lacks permission for LLM settings
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e),
|
||||
)
|
||||
except OrgDatabaseError as e:
|
||||
logger.error(
|
||||
) from e
|
||||
except OrgDatabaseError:
|
||||
logger.exception(
|
||||
'Database operation failed',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to update organization',
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error updating organization',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -934,13 +950,13 @@ async def get_org_members(
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError:
|
||||
logger.exception('Invalid UUID format')
|
||||
logger.exception('Invalid UUID format', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid organization ID format',
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Error retrieving organization members')
|
||||
logger.exception('Error retrieving organization members', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve members',
|
||||
@@ -994,13 +1010,13 @@ async def get_org_members_count(
|
||||
detail='You are not a member of this organization',
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception('Invalid UUID format')
|
||||
logger.exception('Invalid UUID format', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid organization ID format',
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Error retrieving organization member count')
|
||||
logger.exception('Error retrieving organization member count', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve member count',
|
||||
@@ -1093,11 +1109,12 @@ async def get_org_members_financial(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Error retrieving organization member financial data',
|
||||
extra={'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -1320,13 +1337,13 @@ async def remove_org_member(
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError:
|
||||
logger.exception('Invalid UUID format')
|
||||
logger.exception('Invalid UUID format', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid organization or user ID format',
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Error removing organization member')
|
||||
logger.exception('Error removing organization member', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to remove member',
|
||||
@@ -1395,6 +1412,7 @@ async def switch_org(
|
||||
logger.exception(
|
||||
'orgs:switch_org:analytics:failed',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
# Retrieve credits from LiteLLM for the new current org
|
||||
@@ -1406,25 +1424,27 @@ async def switch_org(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except OrgAuthorizationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e),
|
||||
)
|
||||
except OrgDatabaseError as e:
|
||||
logger.error(
|
||||
) from e
|
||||
except OrgDatabaseError:
|
||||
logger.exception(
|
||||
'Database operation failed during organization switch',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to switch organization',
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error switching organization',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -1465,11 +1485,11 @@ async def update_org_member(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail='You are not a member of this organization',
|
||||
)
|
||||
) from e
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='Member not found in this organization',
|
||||
)
|
||||
) from e
|
||||
except CannotModifySelfError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@@ -1501,13 +1521,13 @@ async def update_org_member(
|
||||
detail='Failed to update member',
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception('Invalid UUID format')
|
||||
logger.exception('Invalid UUID format', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid organization or user ID format',
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Error updating organization member')
|
||||
logger.exception('Error updating organization member', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to update member',
|
||||
@@ -1547,7 +1567,7 @@ async def get_git_claims(
|
||||
for claim in claims
|
||||
]
|
||||
except Exception:
|
||||
logger.exception('Error fetching Git organization claims')
|
||||
logger.exception('Error fetching Git organization claims', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to fetch Git organization claims',
|
||||
@@ -1616,7 +1636,7 @@ async def claim_git_organization(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except IntegrityError as e:
|
||||
# Only treat the unique constraint violation as a duplicate claim.
|
||||
# Other integrity errors (e.g. FK violations) should surface as 500s.
|
||||
@@ -1629,14 +1649,14 @@ async def claim_git_organization(
|
||||
git_organization=request.git_organization,
|
||||
)
|
||||
),
|
||||
)
|
||||
logger.exception('Integrity error claiming Git organization')
|
||||
) from e
|
||||
logger.exception('Integrity error claiming Git organization', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to claim Git organization',
|
||||
)
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception('Error claiming Git organization')
|
||||
logger.exception('Error claiming Git organization', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to claim Git organization',
|
||||
@@ -1685,7 +1705,7 @@ async def disconnect_git_organization(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception('Error disconnecting Git organization')
|
||||
logger.exception('Error disconnecting Git organization', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to disconnect Git organization',
|
||||
@@ -1856,11 +1876,12 @@ async def list_org_conversations(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
except Exception as e:
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error listing organization conversations',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -1908,10 +1929,11 @@ async def get_org_conversation_stats(
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error getting organization conversation stats',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -1996,10 +2018,11 @@ async def get_org_conversation_usage_stats(
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error getting organization conversation usage stats',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -2069,10 +2092,11 @@ async def get_org_conversation_user_usage_stats(
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error getting organization user usage stats',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -2232,11 +2256,12 @@ async def export_org_conversations_csv(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=e.message,
|
||||
)
|
||||
except Exception as e:
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error exporting organization conversations',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={'user_id': user_id, 'org_id': str(org_id)},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -2288,15 +2313,15 @@ async def get_org_conversation(
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error getting organization conversation',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'org_id': str(org_id),
|
||||
'conversation_id': conversation_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -2367,15 +2392,15 @@ async def stop_org_conversation(
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Unexpected error stopping organization conversation',
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'org_id': str(org_id),
|
||||
'conversation_id': conversation_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
|
||||
@@ -15,21 +15,21 @@ async def is_ready():
|
||||
async with a_session_maker() as session:
|
||||
await session.execute(text('SELECT 1'))
|
||||
except Exception as e:
|
||||
logger.error(f'Database check failed: {str(e)}')
|
||||
logger.exception('Database check failed', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f'Database is not accessible: {str(e)}',
|
||||
)
|
||||
) from e
|
||||
|
||||
# Check Redis connection
|
||||
try:
|
||||
redis_client = get_redis_client()
|
||||
redis_client.ping()
|
||||
except Exception as e:
|
||||
logger.error(f'Redis check failed: {str(e)}')
|
||||
logger.exception('Redis check failed', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f'Redis cache is not accessible: {str(e)}',
|
||||
)
|
||||
) from e
|
||||
|
||||
return 'OK'
|
||||
|
||||
@@ -185,13 +185,13 @@ async def get_or_create_api_key_for_user(
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'org_id': str(org_id),
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to get or create API key',
|
||||
)
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
'Service created API key for user',
|
||||
|
||||
@@ -52,21 +52,20 @@ async def get_user_app_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except UserNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Unexpected error retrieving user app settings',
|
||||
extra={'error': str(e)},
|
||||
'Unexpected error retrieving user app settings', stack_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to retrieve user app settings',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@user_app_settings_router.post('/app', response_model=UserAppSettingsResponse)
|
||||
@@ -98,18 +97,15 @@ async def update_user_app_settings(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except UserNotFoundError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e),
|
||||
)
|
||||
) from e
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Failed to update user app settings',
|
||||
extra={'error': str(e)},
|
||||
)
|
||||
logger.exception('Failed to update user app settings', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to update user app settings',
|
||||
)
|
||||
) from e
|
||||
|
||||
@@ -331,7 +331,7 @@ async def provision_user(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail='Failed to create Keycloak user (it may already exist)',
|
||||
)
|
||||
) from e
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'provision_user:keycloak_create_unexpected',
|
||||
@@ -340,6 +340,7 @@ async def provision_user(
|
||||
'target_org_id': str(target_org_id),
|
||||
'email': email,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -462,8 +463,8 @@ async def provision_user(
|
||||
'target_org_id': str(target_org_id),
|
||||
'kc_user_id': kc_user_id,
|
||||
'email': email,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
await _rollback_partial_provision(
|
||||
token_manager=token_manager,
|
||||
@@ -475,7 +476,7 @@ async def provision_user(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to finish provisioning user',
|
||||
)
|
||||
) from e
|
||||
|
||||
logger.info(
|
||||
'provision_user:success',
|
||||
@@ -565,6 +566,7 @@ async def _rollback_partial_provision(
|
||||
'target_org_id': str(target_org_id),
|
||||
'openhands_user_id': str(openhands_user_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
# 2. Personal-org cascade. Skip when ``UserStore.create_user`` had
|
||||
@@ -582,6 +584,7 @@ async def _rollback_partial_provision(
|
||||
'kc_user_id': kc_user_id,
|
||||
'openhands_user_id': str(openhands_user_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
# 3. Keycloak user. Always last; runs even when no OpenHands DB
|
||||
|
||||
@@ -123,22 +123,22 @@ class AutomationEventService:
|
||||
event_payload = self._build_event_payload(org_context, payload)
|
||||
await self._send_to_automation_service(provider, org_id, event_payload)
|
||||
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
# Network errors are expected and recoverable
|
||||
logger.error(
|
||||
logger.exception(
|
||||
f'[AutomationEventService] Network error forwarding '
|
||||
f'{provider.value} event (org_id={org_id}): {e}',
|
||||
exc_info=True,
|
||||
f'{provider.value} event (org_id={org_id})',
|
||||
extra={'installation_id': installation_id},
|
||||
stack_info=True,
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# Log unexpected errors. Note: This is a background task, so exceptions
|
||||
# won't surface to the HTTP caller - they're logged for debugging only.
|
||||
logger.error(
|
||||
logger.exception(
|
||||
f'[AutomationEventService] Unexpected error forwarding '
|
||||
f'{provider.value} event (org_id={org_id}): {e}',
|
||||
exc_info=True,
|
||||
f'{provider.value} event (org_id={org_id})',
|
||||
extra={'installation_id': installation_id},
|
||||
stack_info=True,
|
||||
)
|
||||
# Don't re-raise in background task - just log for debugging
|
||||
|
||||
@@ -172,19 +172,19 @@ class AutomationEventService:
|
||||
org_id=org_id,
|
||||
payload=event_payload,
|
||||
)
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
||||
logger.error(
|
||||
except (aiohttp.ClientError, asyncio.TimeoutError):
|
||||
logger.exception(
|
||||
f'[AutomationEventService] Network error forwarding '
|
||||
f'jira_dc event (org_id={org_id}): {e}',
|
||||
exc_info=True,
|
||||
f'jira_dc event (org_id={org_id})',
|
||||
extra={'delivery_id': delivery_id},
|
||||
stack_info=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'[AutomationEventService] Unexpected error forwarding '
|
||||
f'jira_dc event (org_id={org_id}): {e}',
|
||||
exc_info=True,
|
||||
f'jira_dc event (org_id={org_id})',
|
||||
extra={'delivery_id': delivery_id},
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
async def _resolve_org_context(
|
||||
@@ -632,12 +632,14 @@ class AutomationEventService:
|
||||
f'event to org {org_id}: {matched} automations matched'
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
f'[AutomationEventService] Timeout ({AUTOMATION_SERVICE_TIMEOUT}s) '
|
||||
f'forwarding {source} event to automation service'
|
||||
f'forwarding {source} event to automation service',
|
||||
stack_info=True,
|
||||
)
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(
|
||||
except aiohttp.ClientError:
|
||||
logger.exception(
|
||||
f'[AutomationEventService] HTTP error forwarding '
|
||||
f'{source} event to automation service: {e}'
|
||||
f'{source} event to automation service',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
@@ -144,13 +144,13 @@ class EmailService:
|
||||
'response_id': response.get('id') if response else None,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to send invitation email',
|
||||
extra={
|
||||
'invitation_id': invitation_id,
|
||||
'email': to_email,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -1246,14 +1246,14 @@ class OrgConversationService:
|
||||
'conversation_id': conversation_id,
|
||||
'sandbox_id': metadata.sandbox_id,
|
||||
}
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to stop sandbox',
|
||||
extra={
|
||||
'conversation_id': conversation_id,
|
||||
'sandbox_id': metadata.sandbox_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
|
||||
@@ -142,14 +142,14 @@ class OrgInvitationService:
|
||||
invitation_token=invitation.token,
|
||||
invitation_id=invitation.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to send invitation email',
|
||||
extra={
|
||||
'invitation_id': invitation.id,
|
||||
'email': email,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
# Don't fail the invitation creation if email fails
|
||||
# The user can still access via direct link
|
||||
@@ -312,6 +312,7 @@ class OrgInvitationService:
|
||||
'user_id': str(user.id),
|
||||
'org_id': str(invitation.org_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -473,18 +474,18 @@ class OrgInvitationService:
|
||||
invitation.org_id, str(user_id)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Failed to create LiteLLM integration for invitation acceptance',
|
||||
extra={
|
||||
'invitation_id': invitation.id,
|
||||
'user_id': str(user_id),
|
||||
'org_id': str(invitation.org_id),
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise InvitationInvalidError(
|
||||
'Failed to set up organization access. Please try again.'
|
||||
)
|
||||
) from e
|
||||
|
||||
# Step 4.5: Ensure the organization still exists before adding membership
|
||||
org = await OrgStore.get_org_by_id(invitation.org_id)
|
||||
|
||||
@@ -74,13 +74,13 @@ class OrgMemberFinancialService:
|
||||
except httpx.HTTPStatusError as e:
|
||||
# Re-raise auth errors - these indicate configuration issues that need fixing
|
||||
if e.response.status_code in (401, 403):
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'LiteLLM authentication/authorization failed',
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'status_code': e.response.status_code,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
# For other HTTP errors (404, 500, etc.), use graceful degradation
|
||||
|
||||
@@ -96,10 +96,9 @@ class SMTPEmailService:
|
||||
finally:
|
||||
client.quit()
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
'Failed to send SMTP email',
|
||||
extra={'error': str(exc), **extra_payload},
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to send SMTP email', extra={**extra_payload}, stack_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@@ -257,10 +257,10 @@ class LiteLLMProxyModelService(DefaultLLMModelService):
|
||||
exc_info=True,
|
||||
)
|
||||
return response
|
||||
_logger.error(
|
||||
_logger.exception(
|
||||
'Failed to fetch models from the LiteLLM proxy and no '
|
||||
'previous result is cached; returning an empty model list',
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
# Not cached, so the next request retries immediately. Still
|
||||
# union the catalogue so BYOK users can bring a key while the
|
||||
|
||||
@@ -78,7 +78,7 @@ async def create_verified_model(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@api_router.put('/{provider}/{model_name:path}')
|
||||
@@ -124,7 +124,7 @@ async def delete_verified_model(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
class SaaSLLMModelService(DefaultLLMModelService):
|
||||
|
||||
@@ -95,9 +95,13 @@ class OrgService:
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'Error creating LiteLLM integration',
|
||||
extra={'org_id': str(org_id), 'user_id': user_id, 'error': str(e)},
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'user_id': user_id,
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise LiteLLMIntegrationError(f'LiteLLM integration failed: {str(e)}')
|
||||
raise LiteLLMIntegrationError('LiteLLM integration failed') from e
|
||||
|
||||
@staticmethod
|
||||
def create_org_entity(
|
||||
@@ -289,13 +293,13 @@ class OrgService:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Unexpected error in steps 4-6, need to clean up LiteLLM
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Unexpected error during organization creation, initiating cleanup',
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
await OrgService._handle_failure_with_cleanup(
|
||||
org_id, user_id, e, 'Failed to create organization'
|
||||
@@ -330,13 +334,13 @@ class OrgService:
|
||||
return persisted_org
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Database persistence failed, initiating LiteLLM cleanup',
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
await OrgService._handle_failure_with_cleanup(
|
||||
org_id, user_id, e, 'Failed to create organization'
|
||||
@@ -378,9 +382,11 @@ class OrgService:
|
||||
raise OrgDatabaseError(
|
||||
f'{error_message}: {str(original_error)}. '
|
||||
f'Cleanup also failed: {str(cleanup_error)}'
|
||||
)
|
||||
) from original_error
|
||||
|
||||
raise OrgDatabaseError(f'{error_message}: {str(original_error)}')
|
||||
raise OrgDatabaseError(
|
||||
f'{error_message}: {str(original_error)}'
|
||||
) from original_error
|
||||
|
||||
@staticmethod
|
||||
async def _cleanup_litellm_resources(
|
||||
@@ -409,13 +415,13 @@ class OrgService:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Failed to cleanup LiteLLM team (resources may be orphaned)',
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
return e
|
||||
|
||||
@@ -596,15 +602,7 @@ class OrgService:
|
||||
return updated_org
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Failed to update organization',
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
'user_id': user_id,
|
||||
'error': str(e),
|
||||
},
|
||||
)
|
||||
raise OrgDatabaseError(f'Failed to update organization: {str(e)}')
|
||||
raise OrgDatabaseError('Failed to update organization') from e
|
||||
|
||||
@staticmethod
|
||||
async def get_org_credits(user_id: str, org_id: UUID) -> float | None:
|
||||
@@ -835,11 +833,15 @@ class OrgService:
|
||||
# specific failure mode and force a 500.
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Organization deletion failed',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'org_id': str(org_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise OrgDatabaseError(f'Failed to delete organization: {str(e)}')
|
||||
raise OrgDatabaseError('Failed to delete organization') from e
|
||||
|
||||
@staticmethod
|
||||
async def check_byor_export_enabled(
|
||||
@@ -938,8 +940,12 @@ class OrgService:
|
||||
except OrgDatabaseError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Failed to switch user organization',
|
||||
extra={'user_id': user_id, 'org_id': str(org_id), 'error': str(e)},
|
||||
extra={
|
||||
'user_id': user_id,
|
||||
'org_id': str(org_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise OrgDatabaseError(f'Failed to switch organization: {str(e)}')
|
||||
raise OrgDatabaseError('Failed to switch organization') from e
|
||||
|
||||
@@ -788,11 +788,14 @@ class OrgStore:
|
||||
|
||||
return org
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Failed to delete organization - transaction rolled back',
|
||||
extra={'org_id': str(org_id), 'error': str(e)},
|
||||
extra={
|
||||
'org_id': str(org_id),
|
||||
},
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -754,8 +754,8 @@ class SaasSettingsStore(SettingsStore):
|
||||
normalized.append(mp.model_dump())
|
||||
return normalized
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f'Error fetching org marketplaces: {e}')
|
||||
except Exception:
|
||||
logger.exception('Error fetching org marketplaces', stack_info=True)
|
||||
return []
|
||||
|
||||
async def _ensure_api_key(
|
||||
|
||||
@@ -55,9 +55,10 @@ async def main():
|
||||
logger.info(
|
||||
f'Successfully processed PR #{pr.pr_number} from repo {pr.repo_name}'
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Error processing PR #{pr.pr_number} from repo {pr.repo_name}: {str(e)}'
|
||||
f'Error processing PR #{pr.pr_number} from repo {pr.repo_name}',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
logger.info('PR data enrichment process completed')
|
||||
|
||||
@@ -186,7 +186,7 @@ def get_resend_contacts(audience_id: str) -> Dict[str, Dict[str, Any]]:
|
||||
# efficient lookup
|
||||
return {contact['email'].lower(): contact for contact in contacts}
|
||||
except Exception:
|
||||
logger.exception('Failed to get contacts from Resend')
|
||||
logger.exception('Failed to get contacts from Resend', stack_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ def add_contact_to_resend(
|
||||
|
||||
return resend.Contacts.create(params)
|
||||
except Exception:
|
||||
logger.exception(f'Failed to add contact {email} to Resend')
|
||||
logger.exception(f'Failed to add contact {email} to Resend', stack_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ def send_welcome_email(
|
||||
logger.info(f'Welcome email sent to {email}')
|
||||
return response
|
||||
except Exception:
|
||||
logger.exception(f'Failed to send welcome email to {email}')
|
||||
logger.exception(f'Failed to send welcome email to {email}', stack_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@@ -367,7 +367,9 @@ def _backfill_existing_resend_contacts(
|
||||
return backfilled_count
|
||||
|
||||
except Exception:
|
||||
logger.exception('Error during backfill of existing Resend contacts')
|
||||
logger.exception(
|
||||
'Error during backfill of existing Resend contacts', stack_info=True
|
||||
)
|
||||
# Don't fail the entire sync if backfill fails - just log and continue
|
||||
return 0
|
||||
|
||||
@@ -456,7 +458,9 @@ def sync_users_to_resend():
|
||||
user_id=user.id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(f'Failed to mark user {email} as synced')
|
||||
logger.exception(
|
||||
f'Failed to mark user {email} as synced', stack_info=True
|
||||
)
|
||||
stats['errors'] += 1
|
||||
continue
|
||||
|
||||
@@ -466,7 +470,9 @@ def sync_users_to_resend():
|
||||
)
|
||||
logger.info(f'Added user {email} to Resend')
|
||||
except Exception:
|
||||
logger.exception(f'Error adding user {email} to Resend')
|
||||
logger.exception(
|
||||
f'Error adding user {email} to Resend', stack_info=True
|
||||
)
|
||||
synced_user_store.remove_synced_user(email, RESEND_AUDIENCE_ID)
|
||||
stats['errors'] += 1
|
||||
continue
|
||||
@@ -482,7 +488,8 @@ def sync_users_to_resend():
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to send welcome email to {email}, '
|
||||
'but contact was added to audience'
|
||||
'but contact was added to audience',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
time.sleep(1 / RATE_LIMIT)
|
||||
@@ -491,10 +498,10 @@ def sync_users_to_resend():
|
||||
|
||||
logger.info(f'Sync completed: {stats}')
|
||||
except ResendAPIError:
|
||||
logger.exception('Resend API error')
|
||||
logger.exception('Resend API error', stack_info=True)
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
logger.exception('Sync failed with unexpected error')
|
||||
logger.exception('Sync failed with unexpected error', stack_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -698,17 +698,16 @@ class TestReceiveMessagePayloadProcessingError:
|
||||
|
||||
expected_message = '[Github]: Error processing payload for gh interaction'
|
||||
|
||||
# The failure is logged at error level with exc_info so the stack trace is preserved.
|
||||
error_calls = [
|
||||
# The failure is logged at exception level so the stack trace is preserved automatically.
|
||||
exception_calls = [
|
||||
call
|
||||
for call in mock_logger.error.call_args_list
|
||||
for call in mock_logger.exception.call_args_list
|
||||
if call.args and call.args[0] == expected_message
|
||||
]
|
||||
assert error_calls, (
|
||||
f'Expected error log about payload processing. '
|
||||
f'Got: {mock_logger.error.call_args_list}'
|
||||
assert exception_calls, (
|
||||
f'Expected exception log about payload processing. '
|
||||
f'Got: {mock_logger.exception.call_args_list}'
|
||||
)
|
||||
assert all(call.kwargs.get('exc_info') is True for call in error_calls)
|
||||
|
||||
# It must not be logged at warning level (the bug this fix addresses).
|
||||
warning_calls = [
|
||||
|
||||
@@ -890,11 +890,11 @@ class TestSendToAutomationService:
|
||||
ProviderType.GITHUB, org_id, payload
|
||||
)
|
||||
|
||||
mock_logger.error.assert_called()
|
||||
assert 'Timeout' in str(mock_logger.error.call_args)
|
||||
mock_logger.exception.assert_called()
|
||||
assert 'Timeout' in str(mock_logger.exception.call_args)
|
||||
assert (
|
||||
'never delivered' in str(mock_logger.error.call_args).lower()
|
||||
or 'timeout' in str(mock_logger.error.call_args).lower()
|
||||
'never delivered' in str(mock_logger.exception.call_args).lower()
|
||||
or 'timeout' in str(mock_logger.exception.call_args).lower()
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -986,8 +986,8 @@ class TestSendToAutomationService:
|
||||
ProviderType.GITHUB, org_id, payload
|
||||
)
|
||||
|
||||
mock_logger.error.assert_called()
|
||||
assert 'HTTP error' in str(mock_logger.error.call_args)
|
||||
mock_logger.exception.assert_called()
|
||||
assert 'HTTP error' in str(mock_logger.exception.call_args)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_503_logs_error(self, mock_token_manager):
|
||||
|
||||
@@ -388,7 +388,7 @@ async def test_create_org_with_owner_database_failure_triggers_cleanup(
|
||||
|
||||
# Verify cleanup was called
|
||||
assert cleanup_called
|
||||
assert 'Database connection failed' in str(exc_info.value)
|
||||
assert 'Database connection failed' in str(exc_info.value.__cause__)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1137,7 +1137,7 @@ async def test_delete_org_with_cleanup_database_failure(session_maker, owner_rol
|
||||
with pytest.raises(OrgDatabaseError) as exc_info:
|
||||
await OrgService.delete_org_with_cleanup(user_id, org_id)
|
||||
|
||||
assert 'Database connection failed' in str(exc_info.value)
|
||||
assert 'Database connection failed' in str(exc_info.value.__cause__)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1164,7 +1164,7 @@ async def test_delete_org_with_cleanup_unexpected_none_result(
|
||||
with pytest.raises(OrgDatabaseError) as exc_info:
|
||||
await OrgService.delete_org_with_cleanup(user_id, org_id)
|
||||
|
||||
assert 'not found during deletion' in str(exc_info.value)
|
||||
assert 'not found during deletion' in str(exc_info.value.__cause__)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -94,7 +94,9 @@ class AnalyticsService:
|
||||
properties=merged,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('AnalyticsService.capture failed for event=%s', event)
|
||||
logger.exception(
|
||||
'AnalyticsService.capture failed for event=%s', event, stack_info=True
|
||||
)
|
||||
|
||||
def set_person_properties(
|
||||
self,
|
||||
@@ -116,7 +118,9 @@ class AnalyticsService:
|
||||
properties=properties,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('AnalyticsService.set_person_properties failed')
|
||||
logger.exception(
|
||||
'AnalyticsService.set_person_properties failed', stack_info=True
|
||||
)
|
||||
|
||||
def group_identify(
|
||||
self,
|
||||
@@ -142,7 +146,7 @@ class AnalyticsService:
|
||||
distinct_id=self._distinct_id(ctx.user_id),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('AnalyticsService.group_identify failed')
|
||||
logger.exception('AnalyticsService.group_identify failed', stack_info=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Typed event methods
|
||||
@@ -521,7 +525,7 @@ class AnalyticsService:
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('AnalyticsService.identify_user failed')
|
||||
logger.exception('AnalyticsService.identify_user failed', stack_info=True)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Flush and shut down the PostHog client.
|
||||
@@ -531,7 +535,7 @@ class AnalyticsService:
|
||||
try:
|
||||
self._client.shutdown()
|
||||
except Exception:
|
||||
logger.exception('AnalyticsService.shutdown failed')
|
||||
logger.exception('AnalyticsService.shutdown failed', stack_info=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Private helpers
|
||||
|
||||
@@ -401,7 +401,7 @@ async def start_app_conversation(
|
||||
has_repository=start_request.selected_repository is not None,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:conversation_created:failed')
|
||||
logger.exception('analytics:conversation_created:failed', stack_info=True)
|
||||
|
||||
asyncio.create_task(_consume_remaining(async_iter, db_session, httpx_client))
|
||||
return result
|
||||
@@ -569,20 +569,21 @@ async def send_message_to_conversation(
|
||||
)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
f'Agent server returned error when sending message: '
|
||||
f'{e.response.status_code} - {e.response.text}'
|
||||
f'{e.response.status_code} - {e.response.text}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f'Agent server error: {e.response.status_code}',
|
||||
)
|
||||
) from e
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Failed to reach agent server: {e}')
|
||||
logger.exception('Failed to reach agent server', stack_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail='Failed to reach agent server.',
|
||||
)
|
||||
) from e
|
||||
|
||||
return AppSendMessageResponse(
|
||||
success=True,
|
||||
@@ -614,6 +615,7 @@ async def _persist_conversation_model(
|
||||
'Failed to persist new llm_model on conversation %s after model '
|
||||
'switch — chip may be stale until the next refresh.',
|
||||
conversation_id,
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -747,20 +749,23 @@ async def switch_conversation_profile(
|
||||
profile_llm.usage_id,
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Agent server returned error during switch_llm: '
|
||||
f'{e.response.status_code} - {e.response.text}'
|
||||
f'{e.response.status_code} - {e.response.text}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f'Agent server error: {e.response.status_code}',
|
||||
)
|
||||
) from e
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Failed to reach agent server during switch_llm: {e}')
|
||||
logger.exception(
|
||||
'Failed to reach agent server during switch_llm', stack_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail='Failed to reach agent server.',
|
||||
)
|
||||
) from e
|
||||
|
||||
# Persist the new model so the chat header reflects the swap on next fetch.
|
||||
await _persist_conversation_model(
|
||||
@@ -835,9 +840,10 @@ async def switch_conversation_acp_model(
|
||||
request.model,
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Agent server returned error during switch_acp_model: '
|
||||
f'{e.response.status_code} - {e.response.text}'
|
||||
f'{e.response.status_code} - {e.response.text}',
|
||||
stack_info=True,
|
||||
)
|
||||
# Surface agent-server's 400/504 directly (not-ACP, timeout). The
|
||||
# pre-session 409 band-aid is gone as of SDK #3764: a pre-run switch now
|
||||
@@ -846,17 +852,20 @@ async def switch_conversation_acp_model(
|
||||
raise HTTPException(
|
||||
status_code=e.response.status_code,
|
||||
detail=f'Agent server error: {e.response.status_code}',
|
||||
)
|
||||
) from e
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f'Agent server error: {e.response.status_code}',
|
||||
)
|
||||
) from e
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f'Failed to reach agent server during switch_acp_model: {e}')
|
||||
logger.exception(
|
||||
'Failed to reach agent server during switch_acp_model',
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail='Failed to reach agent server.',
|
||||
)
|
||||
) from e
|
||||
|
||||
# Persist so the conversation's model chip reflects the switch on next load.
|
||||
await _persist_conversation_model(
|
||||
@@ -915,7 +924,9 @@ async def _finalize_sandbox_delete(
|
||||
# orphaned row is left; the row + running runtime stay for the runtime-api
|
||||
# idle reap to capture + reap.
|
||||
logger.exception(
|
||||
'Deferred sandbox cleanup failed for %s; kept for retry', sandbox_id
|
||||
'Deferred sandbox cleanup failed for %s; kept for retry',
|
||||
sandbox_id,
|
||||
stack_info=True,
|
||||
)
|
||||
await db_session.rollback()
|
||||
finally:
|
||||
@@ -990,7 +1001,7 @@ async def delete_app_conversation(
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:conversation_deleted:failed')
|
||||
logger.exception('analytics:conversation_deleted:failed', stack_info=True)
|
||||
|
||||
# Commit the deletion
|
||||
await db_session.commit()
|
||||
@@ -1259,28 +1270,39 @@ async def _proxy_git_runtime_call(
|
||||
upstream.raise_for_status()
|
||||
return upstream.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(
|
||||
logger.exception(
|
||||
'Agent server returned error during %s: %s - %s',
|
||||
runtime_path,
|
||||
e.response.status_code,
|
||||
e.response.text,
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f'Agent server error: {e.response.status_code}',
|
||||
)
|
||||
) from e
|
||||
except (json.JSONDecodeError, httpx.DecodingError) as e:
|
||||
logger.error('Agent server returned non-JSON during %s: %s', runtime_path, e)
|
||||
logger.exception(
|
||||
'Agent server returned non-JSON during %s: %s',
|
||||
runtime_path,
|
||||
e,
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail='Agent server returned unexpected response.',
|
||||
)
|
||||
) from e
|
||||
except httpx.RequestError as e:
|
||||
logger.error('Failed to reach agent server during %s: %s', runtime_path, e)
|
||||
logger.exception(
|
||||
'Failed to reach agent server during %s: %s',
|
||||
runtime_path,
|
||||
e,
|
||||
stack_info=True,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail='Failed to reach agent server.',
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
@router.get('/{conversation_id}/git/changes')
|
||||
@@ -1436,7 +1458,10 @@ async def get_conversation_skills(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting skills for conversation {conversation_id}: {e}')
|
||||
logger.exception(
|
||||
f'Error getting skills for conversation {conversation_id}',
|
||||
stack_info=True,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={'error': f'Error getting skills: {str(e)}'},
|
||||
@@ -1580,7 +1605,10 @@ async def get_conversation_hooks(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Error getting hooks for conversation {conversation_id}: {e}')
|
||||
logger.exception(
|
||||
f'Error getting hooks for conversation {conversation_id}',
|
||||
stack_info=True,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={'error': f'Error getting hooks: {str(e)}'},
|
||||
@@ -1633,7 +1661,7 @@ async def export_conversation(
|
||||
conversation_id=str(conversation_id),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:trajectory_downloaded:failed')
|
||||
logger.exception('analytics:trajectory_downloaded:failed', stack_info=True)
|
||||
|
||||
return StreamingResponse(
|
||||
zip_stream,
|
||||
@@ -1643,17 +1671,17 @@ async def export_conversation(
|
||||
},
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
except ConversationExportAlreadyRunning as e:
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
raise HTTPException(status_code=409, detail=str(e)) from e
|
||||
except ConversationExportLockUnavailable as e:
|
||||
raise HTTPException(status_code=503, detail=str(e))
|
||||
raise HTTPException(status_code=503, detail=str(e)) from e
|
||||
except ConversationExportTooLarge as e:
|
||||
raise HTTPException(status_code=413, detail=str(e))
|
||||
raise HTTPException(status_code=413, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f'Failed to download trajectory: {str(e)}'
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
async def _consume_remaining(
|
||||
|
||||
@@ -1018,7 +1018,9 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
fallback_api_key = getattr(settings_llm, 'api_key', None)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'Failed to load profiles for sandbox %s', agent_server_url
|
||||
'Failed to load profiles for sandbox %s',
|
||||
agent_server_url,
|
||||
stack_info=True,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1344,10 +1346,10 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
)
|
||||
mcp_servers.update(user_mcp)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
f'Error loading custom MCP config from user settings: {e}',
|
||||
exc_info=True,
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'Error loading custom MCP config from user settings',
|
||||
stack_info=True,
|
||||
)
|
||||
# Continue with system config only, don't fail conversation startup
|
||||
_logger.warning(
|
||||
@@ -2487,11 +2489,11 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
# AppConversation extends AppConversationInfo, so we can use it directly
|
||||
return await self._delete_from_database(app_conversation)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
f'Error deleting V1 conversation {conversation_id}: {e}',
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
f'Error deleting V1 conversation {conversation_id}',
|
||||
extra={'conversation_id': str(conversation_id)},
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@@ -171,8 +171,8 @@ class DefaultLLMModelService(LLMModelService):
|
||||
resp = await client.get(ollama_url, timeout=3)
|
||||
ollama_models_list = resp.json()['models']
|
||||
extra_models.extend('ollama/' + m['name'] for m in ollama_models_list)
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'Error getting OLLAMA models: {e}')
|
||||
except httpx.HTTPError:
|
||||
_logger.exception('Error getting OLLAMA models', stack_info=True)
|
||||
|
||||
self._cached_response = get_supported_llm_models(
|
||||
verified_models=verified_models,
|
||||
|
||||
@@ -45,10 +45,10 @@ class AwsEventService(EventServiceBase):
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'NoSuchKey':
|
||||
return None
|
||||
_logger.exception(f'Error reading event from {path}')
|
||||
_logger.exception(f'Error reading event from {path}', stack_info=True)
|
||||
return None
|
||||
except Exception:
|
||||
_logger.exception(f'Error reading event from {path}')
|
||||
_logger.exception(f'Error reading event from {path}', stack_info=True)
|
||||
return None
|
||||
|
||||
def _store_event(self, path: Path, event: Event):
|
||||
|
||||
@@ -51,7 +51,7 @@ class GoogleCloudEventService(EventServiceBase):
|
||||
except NotFound:
|
||||
return None
|
||||
except Exception:
|
||||
_logger.exception(f'Error reading event from {path}')
|
||||
_logger.exception(f'Error reading event from {path}', stack_info=True)
|
||||
return None
|
||||
|
||||
def _store_event(self, path: Path, event: Event):
|
||||
|
||||
@@ -529,7 +529,9 @@ async def on_event(
|
||||
conversation_id, app_conversation_info, events, exec_status
|
||||
)
|
||||
except Exception:
|
||||
_logger.exception('analytics:conversation_terminal:failed')
|
||||
_logger.exception(
|
||||
'analytics:conversation_terminal:failed', stack_info=True
|
||||
)
|
||||
|
||||
background_tasks.add_task(
|
||||
_run_callbacks_in_bg_and_close,
|
||||
@@ -633,8 +635,11 @@ def _import_all_tools():
|
||||
if is_pkg: # Check if it's a subpackage
|
||||
try:
|
||||
importlib.import_module(name)
|
||||
except ImportError as e:
|
||||
_logger.error(f"Warning: Could not import subpackage '{name}': {e}")
|
||||
except ImportError:
|
||||
_logger.exception(
|
||||
f"Warning: Could not import subpackage '{name}'",
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
|
||||
_import_all_tools()
|
||||
|
||||
@@ -61,7 +61,7 @@ class GoogleCloudFileStore(FileStore):
|
||||
with blob.open('r') as f:
|
||||
return str(f.read())
|
||||
except NotFound as err:
|
||||
raise FileNotFoundError(err)
|
||||
raise FileNotFoundError(err) from err
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
if not path or path == '/':
|
||||
|
||||
@@ -79,5 +79,5 @@ class LocalFileStore(FileStore):
|
||||
elif os.path.isdir(full_path):
|
||||
shutil.rmtree(full_path)
|
||||
logger.debug(f'Removed local directory: {full_path}')
|
||||
except Exception as e:
|
||||
logger.error(f'Error clearing local file store: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('Error clearing local file store', stack_info=True)
|
||||
|
||||
@@ -48,5 +48,5 @@ class InMemoryFileStore(FileStore):
|
||||
for key in keys_to_delete:
|
||||
del self.files[key]
|
||||
logger.debug(f'Cleared in-memory file store: {path}')
|
||||
except Exception as e:
|
||||
logger.error(f'Error clearing in-memory file store: {str(e)}')
|
||||
except Exception:
|
||||
logger.exception('Error clearing in-memory file store', stack_info=True)
|
||||
|
||||
@@ -66,14 +66,14 @@ class S3FileStore(FileStore):
|
||||
if e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
) from e
|
||||
elif e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
) from e
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}"
|
||||
) from e
|
||||
|
||||
def write_from_path(self, path: str, source_path: str) -> None:
|
||||
# upload_file streams the file from disk in parts; never buffers the whole
|
||||
@@ -84,14 +84,14 @@ class S3FileStore(FileStore):
|
||||
if e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
) from e
|
||||
elif e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
) from e
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}"
|
||||
) from e
|
||||
|
||||
def read(self, path: str) -> str:
|
||||
try:
|
||||
@@ -105,19 +105,19 @@ class S3FileStore(FileStore):
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
) from e
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}"
|
||||
) from e
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
if not path or path == '/':
|
||||
@@ -175,23 +175,23 @@ class S3FileStore(FileStore):
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
) from e
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
) from e
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}': {e}"
|
||||
)
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}'"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}: {e}"
|
||||
)
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}"
|
||||
) from e
|
||||
|
||||
def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None:
|
||||
if not url:
|
||||
|
||||
@@ -212,9 +212,9 @@ class AzureDevOpsService(
|
||||
return response.json(), headers
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
def _parse_repository(self, repository: str) -> tuple[str, str, str]:
|
||||
"""Parse repository string into organization, project, and repo name.
|
||||
|
||||
@@ -51,7 +51,7 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase):
|
||||
body = fields.get('System.Description') or ''
|
||||
return title, body
|
||||
except Exception as wi_error:
|
||||
logger.error(f'Failed to get as work item: {wi_error}')
|
||||
logger.exception(f'Failed to get as work item: {wi_error}', stack_info=True)
|
||||
return '', ''
|
||||
|
||||
async def get_issue_or_pr_comments(
|
||||
@@ -86,7 +86,9 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase):
|
||||
repository, issue_number, max_comments
|
||||
)
|
||||
except Exception as wi_error:
|
||||
logger.error(f'Failed to get work item comments: {wi_error}')
|
||||
logger.exception(
|
||||
f'Failed to get work item comments: {wi_error}', stack_info=True
|
||||
)
|
||||
return []
|
||||
|
||||
async def get_review_thread_comments(
|
||||
@@ -164,5 +166,7 @@ class AzureDevOpsResolverMixin(AzureDevOpsMixinBase):
|
||||
return all_comments[:max_comments]
|
||||
|
||||
except Exception as error:
|
||||
logger.error(f'Failed to get thread {thread_id} comments: {error}')
|
||||
logger.exception(
|
||||
f'Failed to get thread {thread_id} comments: {error}', stack_info=True
|
||||
)
|
||||
return []
|
||||
|
||||
@@ -126,9 +126,9 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
|
||||
response.raise_for_status()
|
||||
return response.json(), dict(response.headers)
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def _fetch_paginated_data(
|
||||
self, url: str, params: dict, max_items: int
|
||||
|
||||
@@ -139,9 +139,9 @@ class BitbucketDCMixinBase(BaseGitService, HTTPClient):
|
||||
data = response.text
|
||||
return data, dict(response.headers)
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def verify_access(self) -> None:
|
||||
"""Verify that the token and host are valid by making a lightweight API call.
|
||||
|
||||
@@ -119,9 +119,9 @@ class ForgejoMixinBase(BaseGitService, HTTPClient):
|
||||
return response.text, headers_out
|
||||
|
||||
except httpx.HTTPStatusError as err:
|
||||
raise self.handle_http_status_error(err)
|
||||
raise self.handle_http_status_error(err) from err
|
||||
except httpx.HTTPError as err:
|
||||
raise self.handle_http_error(err)
|
||||
raise self.handle_http_error(err) from err
|
||||
|
||||
def _resolve_base_url(
|
||||
self,
|
||||
|
||||
@@ -90,9 +90,9 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
|
||||
return response.json(), headers
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def execute_graphql_query(
|
||||
self, query: str, variables: dict[str, Any]
|
||||
@@ -117,9 +117,9 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
|
||||
return dict(result)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def get_user_emails(self) -> list[dict]:
|
||||
"""Fetch the authenticated user's email addresses from GitHub.
|
||||
|
||||
@@ -81,9 +81,9 @@ class GitLabMixinBase(BaseGitService, HTTPClient):
|
||||
return response.text, headers
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def execute_graphql_query(
|
||||
self, query: str, variables: dict[str, Any] | None = None
|
||||
@@ -134,9 +134,9 @@ class GitLabMixinBase(BaseGitService, HTTPClient):
|
||||
|
||||
return result.get('data')
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise self.handle_http_status_error(e)
|
||||
raise self.handle_http_status_error(e) from e
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
raise self.handle_http_error(e) from e
|
||||
|
||||
async def get_user(self) -> User:
|
||||
url = f'{self.BASE_URL}/user'
|
||||
|
||||
@@ -216,10 +216,10 @@ class ProviderHandler:
|
||||
data = TokenResponse.model_validate_json(resp.text)
|
||||
return SecretStr(data.token)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Failed to fetch latest token for provider {provider}: {e}',
|
||||
exc_info=True,
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to fetch latest token for provider {provider}',
|
||||
stack_info=True,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -71,8 +71,8 @@ def init_tavily_proxy() -> None:
|
||||
# Mount under 'tavily' namespace so tools are accessible as tavily_*
|
||||
mcp_server.mount(namespace='tavily', server=proxy_server)
|
||||
logger.info('Tavily MCP proxy initialized successfully')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to initialize Tavily MCP proxy: {e}')
|
||||
except Exception:
|
||||
logger.exception('Failed to initialize Tavily MCP proxy', stack_info=True)
|
||||
|
||||
|
||||
async def get_conversation_link(
|
||||
@@ -207,7 +207,7 @@ async def create_pr(
|
||||
|
||||
except Exception as e:
|
||||
error = f'Error creating pull request: {e}'
|
||||
raise ToolError(str(error))
|
||||
raise ToolError(str(error)) from e
|
||||
|
||||
return response
|
||||
|
||||
@@ -281,7 +281,7 @@ async def create_mr(
|
||||
|
||||
except Exception as e:
|
||||
error = f'Error creating merge request: {e}'
|
||||
raise ToolError(str(error))
|
||||
raise ToolError(str(error)) from e
|
||||
|
||||
return response
|
||||
|
||||
@@ -347,8 +347,8 @@ async def create_bitbucket_pr(
|
||||
|
||||
except Exception as e:
|
||||
error = f'Error creating pull request: {e}'
|
||||
logger.error(error)
|
||||
raise ToolError(str(error))
|
||||
logger.exception(error, stack_info=True)
|
||||
raise ToolError(str(error)) from e
|
||||
|
||||
return response
|
||||
|
||||
@@ -414,8 +414,8 @@ async def create_bitbucket_data_center_pr(
|
||||
|
||||
except Exception as e:
|
||||
error = f'Error creating pull request: {e}'
|
||||
logger.error(error)
|
||||
raise ToolError(str(error))
|
||||
logger.exception(error, stack_info=True)
|
||||
raise ToolError(str(error)) from e
|
||||
|
||||
return response
|
||||
|
||||
@@ -481,7 +481,7 @@ async def create_azure_devops_pr(
|
||||
|
||||
except Exception as e:
|
||||
error = f'Error creating pull request: {e}'
|
||||
logger.error(error)
|
||||
raise ToolError(str(error))
|
||||
logger.exception(error, stack_info=True)
|
||||
raise ToolError(str(error)) from e
|
||||
|
||||
return response
|
||||
|
||||
@@ -80,7 +80,7 @@ async def queue_pending_message(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'Invalid content format: {e}',
|
||||
)
|
||||
) from e
|
||||
|
||||
# Rate limit: max 10 pending messages per conversation
|
||||
pending_count = await pending_service.count_pending_messages(conversation_id)
|
||||
|
||||
@@ -513,7 +513,7 @@ class DockerSandboxService(SandboxService):
|
||||
return sandbox_info
|
||||
|
||||
except APIError as e:
|
||||
raise SandboxError(f'Failed to start container: {e}')
|
||||
raise SandboxError('Failed to start container') from e
|
||||
|
||||
async def resume_sandbox(self, sandbox_id: str) -> bool:
|
||||
"""Resume a paused sandbox."""
|
||||
|
||||
@@ -160,7 +160,7 @@ class ProcessSandboxService(SandboxService):
|
||||
return process
|
||||
|
||||
except Exception as e:
|
||||
raise SandboxError(f'Failed to start agent process: {e}')
|
||||
raise SandboxError('Failed to start agent process') from e
|
||||
|
||||
async def _wait_for_server_ready(self, port: int, timeout: int = 30) -> bool:
|
||||
"""Wait for the agent server to be ready."""
|
||||
|
||||
@@ -143,10 +143,13 @@ class RemoteSandboxService(SandboxService):
|
||||
if attempt + 1 < attempts:
|
||||
_logger.warning(f'Timeout for URL {url}; retrying')
|
||||
continue
|
||||
_logger.error(f'No response received within timeout for URL: {url}')
|
||||
_logger.exception(
|
||||
f'No response received within timeout for URL: {url}',
|
||||
stack_info=True,
|
||||
)
|
||||
raise
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'HTTP error for URL {url}: {e}')
|
||||
except httpx.HTTPError:
|
||||
_logger.exception(f'HTTP error for URL {url}', stack_info=True)
|
||||
raise
|
||||
raise last_exc # type: ignore[misc] # unreachable; keeps mypy happy
|
||||
|
||||
@@ -510,8 +513,8 @@ class RemoteSandboxService(SandboxService):
|
||||
return self._to_sandbox_info(stored_sandbox, runtime_data)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'Failed to start sandbox: {e}')
|
||||
raise SandboxError(f'Failed to start sandbox: {e}')
|
||||
_logger.exception('Failed to start sandbox', stack_info=True)
|
||||
raise SandboxError('Failed to start sandbox') from e
|
||||
|
||||
async def resume_sandbox(self, sandbox_id: str) -> bool:
|
||||
"""Resume a paused sandbox.
|
||||
@@ -550,8 +553,8 @@ class RemoteSandboxService(SandboxService):
|
||||
)
|
||||
|
||||
return True
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'Error resuming sandbox {sandbox_id}: {e}')
|
||||
except httpx.HTTPError:
|
||||
_logger.exception(f'Error resuming sandbox {sandbox_id}', stack_info=True)
|
||||
return False
|
||||
|
||||
async def pause_sandbox(self, sandbox_id: str) -> bool:
|
||||
@@ -580,8 +583,8 @@ class RemoteSandboxService(SandboxService):
|
||||
response.raise_for_status()
|
||||
return True
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'Error pausing sandbox {sandbox_id}: {e}')
|
||||
except httpx.HTTPError:
|
||||
_logger.exception(f'Error pausing sandbox {sandbox_id}', stack_info=True)
|
||||
return False
|
||||
|
||||
async def delete_sandbox(self, sandbox_id: str) -> bool:
|
||||
@@ -644,7 +647,7 @@ class RemoteSandboxService(SandboxService):
|
||||
# signal retryable (503) — never a 404. Persist the key invalidation
|
||||
# now: the caller rolls back on this raise, which would otherwise
|
||||
# restore the hash and leave a just-revoked key valid.
|
||||
_logger.error(f'Error deleting sandbox {sandbox_id}: {e}')
|
||||
_logger.exception(f'Error deleting sandbox {sandbox_id}', stack_info=True)
|
||||
if had_key:
|
||||
await self.db_session.commit()
|
||||
raise SandboxDeleteRetryError(
|
||||
@@ -718,7 +721,9 @@ class RemoteSandboxService(SandboxService):
|
||||
# Could not resolve the workspace layout: never archive to the wrong
|
||||
# path. Honor REQUIRED (block + retry) vs best-effort (proceed).
|
||||
_logger.exception(
|
||||
'Could not resolve archive path for %s', stored_sandbox.id
|
||||
'Could not resolve archive path for %s',
|
||||
stored_sandbox.id,
|
||||
stack_info=True,
|
||||
)
|
||||
return not workspace_archive.archive_required()
|
||||
|
||||
@@ -764,6 +769,7 @@ class RemoteSandboxService(SandboxService):
|
||||
'Workspace archive lookup failed for %s (%s)',
|
||||
sandbox_id,
|
||||
conversation_id,
|
||||
stack_info=True,
|
||||
)
|
||||
return not workspace_archive.archive_required()
|
||||
except Exception:
|
||||
@@ -771,6 +777,7 @@ class RemoteSandboxService(SandboxService):
|
||||
'Workspace archive lookup failed for %s (%s)',
|
||||
sandbox_id,
|
||||
conversation_id,
|
||||
stack_info=True,
|
||||
)
|
||||
return not workspace_archive.archive_required()
|
||||
archived = await self._archive_workspace(
|
||||
|
||||
@@ -525,7 +525,11 @@ async def archive_workspace(
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.exception(
|
||||
'Workspace archive upload (%s) failed for %s: %s', fmt, sandbox_id, e
|
||||
'Workspace archive upload (%s) failed for %s: %s',
|
||||
fmt,
|
||||
sandbox_id,
|
||||
e,
|
||||
stack_info=True,
|
||||
)
|
||||
retryable_failure = True
|
||||
finally:
|
||||
|
||||
@@ -364,7 +364,9 @@ class DbSessionInjector(BaseModel, Injector[AsyncSession]):
|
||||
if asyncio.iscoroutine(result):
|
||||
await result
|
||||
except Exception:
|
||||
_logger.exception('Error closing GCP Cloud SQL connector')
|
||||
_logger.exception(
|
||||
'Error closing GCP Cloud SQL connector', stack_info=True
|
||||
)
|
||||
self._gcp_connector = None
|
||||
|
||||
engine = self._async_engine
|
||||
@@ -372,7 +374,7 @@ class DbSessionInjector(BaseModel, Injector[AsyncSession]):
|
||||
try:
|
||||
await engine.dispose()
|
||||
except Exception:
|
||||
_logger.exception('Error disposing async DB engine')
|
||||
_logger.exception('Error disposing async DB engine', stack_info=True)
|
||||
|
||||
|
||||
def set_db_session_keep_open(state: InjectorState, keep_open: bool):
|
||||
|
||||
@@ -142,7 +142,7 @@ class JwtService:
|
||||
payload = jwt.decode(token, secret_key, algorithms=['HS256'])
|
||||
return payload
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise jwt.InvalidTokenError(f'Token verification failed: {str(e)}')
|
||||
raise jwt.InvalidTokenError('Token verification failed') from e
|
||||
|
||||
def create_jwe_token(
|
||||
self,
|
||||
@@ -244,7 +244,7 @@ class JwtService:
|
||||
raise ValueError('Decryption produced no plaintext')
|
||||
return json.loads(result.plaintext)
|
||||
except Exception as e:
|
||||
raise Exception(f'Token decryption failed: {str(e)}')
|
||||
raise Exception('Token decryption failed') from e
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Symmetric encrypt / decrypt helpers (JWE with legacy Fernet fallback)
|
||||
|
||||
@@ -321,7 +321,7 @@ async def store_settings(
|
||||
settings_changed=settings_changed,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('analytics:settings_saved:failed')
|
||||
logger.exception('analytics:settings_saved:failed', stack_info=True)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
|
||||
@@ -430,13 +430,15 @@ async def get_marketplace_skills(
|
||||
marketplace_skills[marketplace.name] = skill_names
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f'Unexpected error in marketplace-skills endpoint: {e}')
|
||||
logger.exception(
|
||||
'Unexpected error in marketplace-skills endpoint', stack_info=True
|
||||
)
|
||||
errors.append(f'Internal error: {str(e)}')
|
||||
# Clean up before raising
|
||||
for clone_dir in cloned_dirs:
|
||||
_cleanup_clone_dir(clone_dir)
|
||||
# Raise HTTP 500 for critical errors
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
finally:
|
||||
# Clean up cloned directories
|
||||
for clone_dir in cloned_dirs:
|
||||
|
||||
@@ -154,8 +154,11 @@ def resolve_llm_base_url(
|
||||
return managed_proxy_url
|
||||
try:
|
||||
return get_provider_api_base(model)
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to get api_base from litellm for model {model}: {e}')
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f'Failed to get api_base from litellm for model {model}',
|
||||
stack_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def _register_signal_handler(sig: signal.Signals) -> None:
|
||||
try:
|
||||
callable()
|
||||
except Exception:
|
||||
logger.exception('Error calling shutdown listener')
|
||||
logger.exception('Error calling shutdown listener', stack_info=True)
|
||||
if original_handler:
|
||||
original_handler(sig_, frame) # type: ignore[unreachable]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user