Skip to content

Commit

Permalink
Merge pull request #489 from MrRoudyk/feature/481-add-bug-report-endp…
Browse files Browse the repository at this point in the history
…oint

feat: add bug report API with validation and Sentry integration
  • Loading branch information
djeck1432 authored Jan 23, 2025
2 parents 63aecc6 + 657daa6 commit 0121a27
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 4 deletions.
27 changes: 26 additions & 1 deletion web_app/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
This module defines the serializers for the user data.
"""

from decimal import Decimal
from datetime import datetime
from decimal import Decimal

from pydantic import BaseModel, Field


Expand Down Expand Up @@ -100,3 +101,27 @@ class SubscribeToNotificationRequest(BaseModel):
None, example="123456789", description="Telegram ID of the user"
)
wallet_id: str = Field(..., example="0xabc123", description="Wallet ID of the user")


class BugReportRequest(BaseModel):
"""
Pydantic model for bug report request.
"""

wallet_id: str = Field(
..., pattern=r"^0x[a-fA-F0-9]+$", description="User's wallet ID"
)
telegram_id: str | None = Field(
None, pattern=r"^\d+$", description="User's Telegram ID"
)
bug_description: str = Field(
..., min_length=1, description="Description of the bug"
)


class BugReportResponse(BaseModel):
"""
Pydantic model for bug report response.
"""

message: str = Field(..., example="Bug report submitted successfully")
68 changes: 65 additions & 3 deletions web_app/api/user.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@

"""
This module handles user-related API endpoints.
"""

import logging
from decimal import Decimal

import sentry_sdk
from fastapi import APIRouter, HTTPException

from web_app.api.serializers.transaction import UpdateUserContractRequest
from web_app.api.serializers.user import (
BugReportRequest,
BugReportResponse,
CheckUserResponse,
GetStatsResponse,
GetUserContractAddressResponse,
SubscribeToNotificationRequest,
UpdateUserContractResponse,
)
from web_app.contract_tools.mixins import PositionMixin, DashboardMixin
from web_app.contract_tools.blockchain_call import CLIENT
from web_app.contract_tools.mixins import DashboardMixin, PositionMixin
from web_app.db.crud import (
PositionDBConnector,
TelegramUserDBConnector,
UserDBConnector,
)
from web_app.contract_tools.blockchain_call import CLIENT

logger = logging.getLogger(__name__)
router = APIRouter() # Initialize the router
Expand Down Expand Up @@ -257,3 +260,62 @@ async def get_stats() -> GetStatsResponse:
except Exception as e:
logger.error(f"Error in get_stats: {e}")
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")


@router.post(
"/api/save-bug-report",
response_model=BugReportResponse,
tags=["Bug Reports"],
summary="Submit a bug report",
response_description="Returns confirmation of bug report submission",
)
async def save_bug_report(report: BugReportRequest) -> BugReportResponse:
"""
Save a bug report and send it to Sentry for tracking.
### Parameters:
- **report**: An instance of BugReportRequest containing:
- **wallet_id**: User's wallet ID (0x format)
- **telegram_id**: Optional Telegram ID (numeric)
- **bug_description**: Detailed bug description (1-1000 chars)
### Returns:
- BugReportResponse with confirmation message
### Raises:
- 422: Validation error
- 500: Internal server error
"""
try:
if report.wallet_id.strip() == "" or report.bug_description.strip() == "":
raise HTTPException(
status_code=422, detail="Wallet ID and bug description cannot be empty"
)

sentry_sdk.set_user(
{"wallet_id": report.wallet_id, "telegram_id": report.telegram_id}
)

sentry_sdk.set_context(
"bug_report",
{
"wallet_id": report.wallet_id,
"telegram_id": report.telegram_id,
"description": report.bug_description,
},
)

sentry_sdk.capture_message(
f"Bug Report from {report.wallet_id}",
level="error",
extras={"description": report.bug_description},
)

return BugReportResponse(message="Bug report submitted successfully")
except Exception as e:
if isinstance(e, HTTPException):
raise

logger.error(f"Failed to submit bug report: {str(e)}")
sentry_sdk.capture_exception(e)
raise HTTPException(status_code=500, detail="Failed to submit bug report")
88 changes: 88 additions & 0 deletions web_app/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,91 @@ async def test_subscribe_to_notification(
# mock_get_contract_address.assert_called_once_with(wallet_id)
# if contract_address:
# mock_withdraw_all.assert_called_once_with(contract_address)


@pytest.mark.asyncio
@patch("sentry_sdk.capture_message")
@patch("sentry_sdk.set_user")
@patch("sentry_sdk.set_context")
@pytest.mark.parametrize(
"report_data, expected_status, expected_response",
[
(
{
"wallet_id": "0x123",
"telegram_id": "456",
"bug_description": "Test bug description",
},
200,
{"message": "Bug report submitted successfully"},
),
(
{"wallet_id": "0x123", "bug_description": "Test without telegram"},
200,
{"message": "Bug report submitted successfully"},
),
],
)
async def test_save_bug_report_success(
mock_set_context,
mock_set_user,
mock_capture_message,
client,
report_data,
expected_status,
expected_response,
):
"""Test successful bug report submission"""
response = client.post("/api/save-bug-report", json=report_data)

assert response.status_code == expected_status
assert response.json() == expected_response
mock_set_user.assert_called_once()
mock_set_context.assert_called_once()
mock_capture_message.assert_called_once()


@pytest.mark.asyncio
@pytest.mark.parametrize(
"report_data, expected_status, error_message",
[
(
{"wallet_id": "invalid", "bug_description": "Test"},
422,
"String should match pattern '^0x[a-fA-F0-9]+$'",
),
(
{"wallet_id": "0x123", "telegram_id": "abc", "bug_description": "Test"},
422,
"String should match pattern '^\\d+$'",
),
(
{"wallet_id": "0x123", "bug_description": ""},
422,
"String should have at least 1 character",
),
(
{"telegram_id": "456", "bug_description": "Missing wallet"},
422,
"Field required",
),
(
{"wallet_id": "0x123", "telegram_id": "456"},
422,
"Field required",
),
(
{},
422,
"Field required",
),
],
)
async def test_save_bug_report_validation(
client, report_data, expected_status, error_message
):
"""Test bug report validation failures"""
response = client.post("/api/save-bug-report", json=report_data)

assert response.status_code == expected_status
assert response.json()["detail"][0]["msg"] == error_message

0 comments on commit 0121a27

Please sign in to comment.