Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions app/google_docs/services/folder_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.google_docs.utils.constants import (
AUTOMATED_DOCS_FOLDER_NAME,
DEFAULT_MAX_RECURSION_DEPTH,
DRIVE_INACCESSIBLE_STATUSES,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -116,7 +117,7 @@ def _search_folder_recursive(

except HttpError as e:
# Log warning for permission/access issues but continue searching
if e.resp.status in [403, 404]:
if e.resp.status in DRIVE_INACCESSIBLE_STATUSES:
logger.warning(
f"Access denied or folder not found {parent_folder_id}: {e}",
)
Expand Down Expand Up @@ -156,7 +157,29 @@ async def get_automated_docs_folder(
Exception: If folder is not found or API call errors occur.

"""
drive_service = self.auth_service.get_drive_service()
drive_service: Any = self.auth_service.get_drive_service()

# Checked first because the recursive search swallows 403/404: an
# unshared parent would otherwise be reported as a missing subfolder.
try:
drive_service.files().get(
fileId=parent_folder_id,
fields="id",
supportsAllDrives=True,
).execute()
except HttpError as exc:
# Relabelling a 5xx as a sharing problem sends the user to fix
# the wrong thing, so only report what this probe can detect.
if exc.resp.status not in DRIVE_INACCESSIBLE_STATUSES:
raise

log_and_raise(
logger,
f"Cannot access Drive folder {parent_folder_id}. Share it with "
f"{self.auth_service.service_account_email} first.",
Exception,
cause=exc,
)

# Try to find the folder recursively
folder_id = self._search_folder_recursive(
Expand Down
9 changes: 9 additions & 0 deletions app/google_docs/services/google_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ def _get_credentials(self) -> service_account.Credentials:

return self._credentials

@property
def service_account_email(self) -> str:
"""The address a Drive folder must be shared with for reports to work."""
try:
info = json.loads(settings.GOOGLE_SERVICE_ACCOUNT_KEY.get_secret_value())
except (json.JSONDecodeError, ValueError):
return "the report service account"
return str(info.get("client_email") or "the report service account")

def get_drive_service(self) -> Resource:
"""Get an authenticated Google Drive API service.

Expand Down
4 changes: 4 additions & 0 deletions app/google_docs/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
# Folder management
DEFAULT_MAX_RECURSION_DEPTH = 10 # Maximum depth for recursive folder search

# Drive hides what the caller can't see, so a 404 here means "not shared"
# rather than "deleted". The search and the parent probe must agree on these.
DRIVE_INACCESSIBLE_STATUSES = (403, 404)

# TODO(namankhare): https://github.com/rtCamp/rt-report-automation/issues/67
# The folder name is subject to change and will be updated once the final
# naming decision is made.
Expand Down
92 changes: 73 additions & 19 deletions app/inngest_proxy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from fastapi import APIRouter

from app.inngest_proxy.models import RunStatusResponse
from app.inngest_proxy.service import InngestProxyService

router = APIRouter(
Expand All @@ -18,39 +19,92 @@
description=(
"Check the status of an Inngest run by event ID. "
"Proxies the request server-side to the Inngest API to avoid "
"browser CORS restrictions."
"browser CORS restrictions, and normalizes the result into a "
"toast-ready status with structured failure detail."
),
response_model=RunStatusResponse,
Comment thread
namankhare marked this conversation as resolved.
response_model_exclude_none=False,
responses={
200: {
"description": "Inngest run status retrieved successfully",
"description": "Run status retrieved successfully",
"content": {
"application/json": {
"example": {
"data": [
{
"examples": {
"completed": {
"summary": "Report generated",
"value": {
"event_id": "01KPZ2GTFVR7X2X4V0B1Q9QS6X",
"run_id": "01KPZ2GTK23KKQBD1Y3NYYHC8E",
"run_started_at": "2026-04-24T06:23:32.962Z",
"function_id": "9577baf4-7d44-572b-acb4-b04cf05e487d",
"function_version": 0,
"environment_id": "00000000-0000-0000-0000-000000000000", # noqa: E501
"status": "completed",
"is_terminal": True,
"message": "Report generated successfully.",
"document_url": "https://docs.google.com/document/d/example/edit",
"error": None,
},
},
"failed_user_fixable": {
"summary": "Failed for a reason the PM can fix",
"value": {
"event_id": "01KPZ2GTFVR7X2X4V0B1Q9QS6X",
"status": "Completed",
"ended_at": "2026-04-24T06:24:18.984143Z",
"output": {
"document_url": "https://docs.google.com/document/d/example/edit",
"run_id": "01KPZ2GTK23KKQBD1Y3NYYHC8E",
"status": "failed",
"is_terminal": True,
"message": (
"The report bot does not have access to the "
"Slack channel. Invite the bot to the channel, "
"then try again."
),
"document_url": None,
"error": {
"error_code": "slack_access_denied",
"user_message": (
"The report bot does not have access to "
"the Slack channel."
),
"action": (
"Invite the bot to the channel, then try again."
),
"is_user_fixable": True,
"technical_detail": "SlackApiError: not_in_channel",
"trace_id": "01KPZ2GTK23KKQBD1Y3NYYHC8E",
"occurred_at": "2026-04-24T06:24:18.984143Z",
},
},
},
"failed_unknown": {
"summary": "Unrecognised failure, escalate with trace ID",
"value": {
"event_id": "01KPZ2GTFVR7X2X4V0B1Q9QS6X",
"run_id": "01KPZ2GTK23KKQBD1Y3NYYHC8E",
"status": "failed",
"is_terminal": True,
"message": (
"Report generation failed for an unexpected "
"reason. Share the trace ID with engineering "
"so they can check the logs."
),
"document_url": None,
"error": {
"error_code": "unknown",
"user_message": (
"Report generation failed for an unexpected "
"reason. Share the trace ID with engineering "
"so they can check the logs."
),
"action": None,
"is_user_fixable": False,
"technical_detail": "RuntimeError: unexpected",
"trace_id": "01KPZ2GTK23KKQBD1Y3NYYHC8E",
"occurred_at": "2026-04-24T06:24:18.984143Z",
},
}
],
"metadata": {
"fetched_at": "2026-04-24T12:29:19.445086Z",
"cached_until": "2026-04-24T12:29:34.445086Z",
},
},
},
},
},
}
},
)
async def get_run_status(event_id: str):
async def get_run_status(event_id: str) -> RunStatusResponse:
"""Proxy endpoint to check Inngest run status by event ID."""
return await inngest_proxy_service.get_run_status(event_id)
Loading