bug_report.py 4.6 KB

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