diff --git a/web_app/api/serializers/user.py b/web_app/api/serializers/user.py index a1464d918..6aab43606 100644 --- a/web_app/api/serializers/user.py +++ b/web_app/api/serializers/user.py @@ -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 @@ -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") diff --git a/web_app/api/user.py b/web_app/api/user.py index de6b4d065..40bf5eb8e 100644 --- a/web_app/api/user.py +++ b/web_app/api/user.py @@ -1,4 +1,3 @@ - """ This module handles user-related API endpoints. """ @@ -6,22 +5,26 @@ 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 @@ -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") diff --git a/web_app/tests/test_user.py b/web_app/tests/test_user.py index 0bc20ff95..59d1405fe 100644 --- a/web_app/tests/test_user.py +++ b/web_app/tests/test_user.py @@ -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