bug_report.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """Bug report service — posts to the bambuddy.cool relay which holds the GitHub PAT."""
  2. import logging
  3. import time
  4. import httpx
  5. from backend.app.core.config import BUG_REPORT_RELAY_URL
  6. from backend.app.core.database import async_session
  7. from backend.app.models.bug_report import BugReport
  8. # The reporter's email goes to the relay with the report (it is how the report
  9. # gets answered) and is not kept in bug_reports: nothing reads that table.
  10. logger = logging.getLogger(__name__)
  11. # Rate limiting: max 5 reports per hour
  12. _rate_limit_window = 3600
  13. _rate_limit_max = 5
  14. _rate_limit_timestamps: list[float] = []
  15. def _check_rate_limit() -> bool:
  16. """Check if rate limit allows a new report. Returns True if allowed."""
  17. now = time.time()
  18. _rate_limit_timestamps[:] = [t for t in _rate_limit_timestamps if now - t < _rate_limit_window]
  19. if len(_rate_limit_timestamps) >= _rate_limit_max:
  20. return False
  21. _rate_limit_timestamps.append(now)
  22. return True
  23. async def submit_report(
  24. description: str,
  25. reporter_email: str | None,
  26. screenshot_base64: str | None,
  27. support_info: dict | None,
  28. ) -> dict:
  29. """Submit a bug report via the bambuddy.cool relay."""
  30. if not _check_rate_limit():
  31. return {
  32. "success": False,
  33. "message": "Rate limit exceeded. Please try again later.",
  34. "issue_url": None,
  35. "issue_number": None,
  36. }
  37. if not BUG_REPORT_RELAY_URL:
  38. return {
  39. "success": False,
  40. "message": "Bug reporting is not configured. BUG_REPORT_RELAY_URL is not set.",
  41. "issue_url": None,
  42. "issue_number": None,
  43. }
  44. # Build relay payload — email is sent to relay for maintainer notification + issue body
  45. payload: dict = {"description": description}
  46. if reporter_email:
  47. payload["reporter_email"] = reporter_email
  48. if screenshot_base64:
  49. payload["screenshot_base64"] = screenshot_base64
  50. if support_info:
  51. payload["support_info"] = support_info
  52. try:
  53. async with httpx.AsyncClient(timeout=60.0) as client:
  54. resp = await client.post(BUG_REPORT_RELAY_URL, json=payload)
  55. if resp.status_code != 200:
  56. error_msg = f"Relay returned HTTP {resp.status_code}"
  57. logger.error("%s at %s", error_msg, BUG_REPORT_RELAY_URL)
  58. async with async_session() as db:
  59. report = BugReport(
  60. description=description,
  61. status="failed",
  62. error_message=error_msg,
  63. )
  64. db.add(report)
  65. await db.commit()
  66. return {
  67. "success": False,
  68. "message": "Bug report relay is not available. Please try again later.",
  69. "issue_url": None,
  70. "issue_number": None,
  71. }
  72. relay_data = resp.json()
  73. except Exception:
  74. logger.exception("Failed to reach bug report relay at %s", BUG_REPORT_RELAY_URL)
  75. async with async_session() as db:
  76. report = BugReport(
  77. description=description,
  78. status="failed",
  79. error_message="Failed to reach bug report relay",
  80. )
  81. db.add(report)
  82. await db.commit()
  83. return {
  84. "success": False,
  85. "message": "Failed to submit bug report. Please try again later.",
  86. "issue_url": None,
  87. "issue_number": None,
  88. }
  89. if not relay_data.get("success"):
  90. async with async_session() as db:
  91. report = BugReport(
  92. description=description,
  93. status="failed",
  94. error_message=relay_data.get("message", "Relay returned failure"),
  95. )
  96. db.add(report)
  97. await db.commit()
  98. return {
  99. "success": False,
  100. "message": relay_data.get("message", "Failed to create bug report."),
  101. "issue_url": None,
  102. "issue_number": None,
  103. }
  104. issue_number = relay_data["issue_number"]
  105. issue_url = relay_data["issue_url"]
  106. # Save to DB
  107. async with async_session() as db:
  108. report = BugReport(
  109. description=description,
  110. github_issue_number=issue_number,
  111. github_issue_url=issue_url,
  112. status="submitted",
  113. email_sent=True,
  114. )
  115. db.add(report)
  116. await db.commit()
  117. return {
  118. "success": True,
  119. "message": "Bug report submitted successfully!",
  120. "issue_url": issue_url,
  121. "issue_number": issue_number,
  122. }