From 1623eed240037334199c1379ba2a2f57e6b4ef7b Mon Sep 17 00:00:00 2001 From: codegen-bot Date: Sat, 15 Mar 2025 18:38:26 +0000 Subject: [PATCH 1/2] CG-12173: Add Sentry Integration with ViewSentryTool --- .../extensions/langchain/sentry_tools.py | 315 ++++++++++++ src/codegen/extensions/langchain/tools.py | 10 +- src/codegen/extensions/sentry/README.md | 138 ++++++ src/codegen/extensions/sentry/__init__.py | 1 + src/codegen/extensions/sentry/config.py | 56 +++ src/codegen/extensions/sentry/example.ipynb | 226 +++++++++ .../extensions/sentry/sentry_client.py | 232 +++++++++ .../extensions/tools/sentry/__init__.py | 1 + .../extensions/tools/sentry/sentry_tool.py | 111 +++++ src/codegen/extensions/tools/sentry/tools.py | 467 ++++++++++++++++++ 10 files changed, 1555 insertions(+), 2 deletions(-) create mode 100644 src/codegen/extensions/langchain/sentry_tools.py create mode 100644 src/codegen/extensions/sentry/README.md create mode 100644 src/codegen/extensions/sentry/__init__.py create mode 100644 src/codegen/extensions/sentry/config.py create mode 100644 src/codegen/extensions/sentry/example.ipynb create mode 100644 src/codegen/extensions/sentry/sentry_client.py create mode 100644 src/codegen/extensions/tools/sentry/__init__.py create mode 100644 src/codegen/extensions/tools/sentry/sentry_tool.py create mode 100644 src/codegen/extensions/tools/sentry/tools.py diff --git a/src/codegen/extensions/langchain/sentry_tools.py b/src/codegen/extensions/langchain/sentry_tools.py new file mode 100644 index 000000000..00f91cab8 --- /dev/null +++ b/src/codegen/extensions/langchain/sentry_tools.py @@ -0,0 +1,315 @@ +"""LangChain tools for Sentry integration.""" + +from typing import ClassVar, Dict, List, Optional + +from langchain_core.tools.base import BaseTool +from pydantic import BaseModel, Field + +from codegen.extensions.sentry.config import DEFAULT_ORGANIZATION_SLUG +from codegen.extensions.tools.sentry.tools import ( + view_sentry_event_details, + view_sentry_issue_details, + view_sentry_issues, +) +from codegen.sdk.core.codebase import Codebase + + +class ViewSentryIssuesInput(BaseModel): + """Input for viewing Sentry issues.""" + + organization_slug: str = Field( + default=DEFAULT_ORGANIZATION_SLUG, + description="Organization slug (e.g., 'codegen-sh', 'ramp')", + ) + project_slug: Optional[str] = Field( + default=None, + description="Optional project slug to filter by (e.g., 'codegen', 'api')", + ) + query: Optional[str] = Field( + default=None, + description="Optional search query to filter issues", + ) + status: str = Field( + default="unresolved", + description="Status filter (e.g., 'resolved', 'unresolved', 'ignored')", + ) + limit: int = Field( + default=20, + description="Maximum number of issues to return", + ) + cursor: Optional[str] = Field( + default=None, + description="Pagination cursor for fetching the next page of results", + ) + + +class ViewSentryIssueDetailsInput(BaseModel): + """Input for viewing a specific Sentry issue.""" + + issue_id: str = Field( + ..., + description="ID of the issue to view", + ) + organization_slug: str = Field( + default=DEFAULT_ORGANIZATION_SLUG, + description="Organization slug (e.g., 'codegen-sh', 'ramp')", + ) + limit: int = Field( + default=10, + description="Maximum number of events to return", + ) + cursor: Optional[str] = Field( + default=None, + description="Pagination cursor for fetching the next page of events", + ) + + +class ViewSentryEventDetailsInput(BaseModel): + """Input for viewing a specific Sentry event.""" + + event_id: str = Field( + ..., + description="ID of the event to view", + ) + organization_slug: str = Field( + default=DEFAULT_ORGANIZATION_SLUG, + description="Organization slug (e.g., 'codegen-sh', 'ramp')", + ) + project_slug: str = Field( + default="codegen", + description="Project slug (e.g., 'codegen', 'api')", + ) + + +class ViewSentryIssuesTool(BaseTool): + """Tool for viewing Sentry issues.""" + + name: ClassVar[str] = "view_sentry_issues" + description: ClassVar[str] = """ + View a list of Sentry issues for an organization or project. + + This tool allows you to retrieve and filter Sentry issues by organization, project, status, and search query. + Results are paginated, and you can use the cursor parameter to fetch additional pages. + """ + args_schema: ClassVar[type[BaseModel]] = ViewSentryIssuesInput + codebase: Codebase = Field(exclude=True) + + def __init__(self, codebase: Codebase) -> None: + super().__init__(codebase=codebase) + + def _run( + self, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + project_slug: Optional[str] = None, + query: Optional[str] = None, + status: str = "unresolved", + limit: int = 20, + cursor: Optional[str] = None, + ) -> str: + result = view_sentry_issues( + codebase=self.codebase, + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + limit=limit, + cursor=cursor, + ) + return result.render() + + +class ViewSentryIssueDetailsTool(BaseTool): + """Tool for viewing details of a specific Sentry issue.""" + + name: ClassVar[str] = "view_sentry_issue" + description: ClassVar[str] = """ + View detailed information about a specific Sentry issue, including its events. + + This tool retrieves comprehensive information about a Sentry issue, including its metadata and associated events. + Events are paginated, and you can use the cursor parameter to fetch additional pages. + """ + args_schema: ClassVar[type[BaseModel]] = ViewSentryIssueDetailsInput + codebase: Codebase = Field(exclude=True) + + def __init__(self, codebase: Codebase) -> None: + super().__init__(codebase=codebase) + + def _run( + self, + issue_id: str, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + limit: int = 10, + cursor: Optional[str] = None, + ) -> str: + result = view_sentry_issue_details( + codebase=self.codebase, + issue_id=issue_id, + organization_slug=organization_slug, + limit=limit, + cursor=cursor, + ) + return result.render() + + +class ViewSentryEventDetailsTool(BaseTool): + """Tool for viewing details of a specific Sentry event.""" + + name: ClassVar[str] = "view_sentry_event" + description: ClassVar[str] = """ + View detailed information about a specific Sentry event. + + This tool retrieves comprehensive information about a Sentry event, including its metadata, tags, user information, + and stack trace (if available). + """ + args_schema: ClassVar[type[BaseModel]] = ViewSentryEventDetailsInput + codebase: Codebase = Field(exclude=True) + + def __init__(self, codebase: Codebase) -> None: + super().__init__(codebase=codebase) + + def _run( + self, + event_id: str, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + project_slug: str = "codegen", + ) -> str: + result = view_sentry_event_details( + codebase=self.codebase, + event_id=event_id, + organization_slug=organization_slug, + project_slug=project_slug, + ) + return result.render() + + +class ViewSentryToolInput(BaseModel): + """Input for the combined ViewSentryTool.""" + + action: str = Field( + default="view_issues", + description="Action to perform: 'view_issues', 'view_issue', or 'view_event'", + ) + organization_slug: str = Field( + default=DEFAULT_ORGANIZATION_SLUG, + description="Organization slug (e.g., 'codegen-sh', 'ramp')", + ) + project_slug: Optional[str] = Field( + default=None, + description="Project slug (e.g., 'codegen', 'api'). Required for 'view_event' action.", + ) + issue_id: Optional[str] = Field( + default=None, + description="ID of the issue to view. Required for 'view_issue' action.", + ) + event_id: Optional[str] = Field( + default=None, + description="ID of the event to view. Required for 'view_event' action.", + ) + query: Optional[str] = Field( + default=None, + description="Search query for filtering issues. Only used with 'view_issues' action.", + ) + status: str = Field( + default="unresolved", + description="Status filter for issues (e.g., 'resolved', 'unresolved'). Only used with 'view_issues' action.", + ) + limit: int = Field( + default=20, + description="Maximum number of results to return. Used with 'view_issues' and 'view_issue' actions.", + ) + cursor: Optional[str] = Field( + default=None, + description="Pagination cursor for fetching the next page of results. Used with 'view_issues' and 'view_issue' actions.", + ) + + +class ViewSentryTool(BaseTool): + """Combined tool for viewing Sentry issues and events.""" + + name: ClassVar[str] = "view_sentry" + description: ClassVar[str] = """ + View Sentry issues and events. + + This tool allows you to: + 1. View a list of Sentry issues for an organization or project + 2. View details of a specific Sentry issue, including its events + 3. View details of a specific Sentry event, including stack traces + + Specify the action parameter to choose which operation to perform: + - 'view_issues': List issues (default) + - 'view_issue': View a specific issue (requires issue_id) + - 'view_event': View a specific event (requires event_id and project_slug) + """ + args_schema: ClassVar[type[BaseModel]] = ViewSentryToolInput + codebase: Codebase = Field(exclude=True) + + def __init__(self, codebase: Codebase) -> None: + super().__init__(codebase=codebase) + + def _run( + self, + action: str = "view_issues", + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + project_slug: Optional[str] = None, + issue_id: Optional[str] = None, + event_id: Optional[str] = None, + query: Optional[str] = None, + status: str = "unresolved", + limit: int = 20, + cursor: Optional[str] = None, + ) -> str: + if action == "view_issues": + result = view_sentry_issues( + codebase=self.codebase, + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + limit=limit, + cursor=cursor, + ) + elif action == "view_issue": + if not issue_id: + return "Error: Missing required parameter 'issue_id' for action 'view_issue'" + + result = view_sentry_issue_details( + codebase=self.codebase, + issue_id=issue_id, + organization_slug=organization_slug, + limit=limit, + cursor=cursor, + ) + elif action == "view_event": + if not event_id: + return "Error: Missing required parameter 'event_id' for action 'view_event'" + + if not project_slug: + return "Error: Missing required parameter 'project_slug' for action 'view_event'" + + result = view_sentry_event_details( + codebase=self.codebase, + event_id=event_id, + organization_slug=organization_slug, + project_slug=project_slug, + ) + else: + return f"Error: Unknown action '{action}'. Supported actions: 'view_issues', 'view_issue', 'view_event'" + + return result.render() + + +def get_sentry_tools(codebase: Codebase) -> List[BaseTool]: + """Get all Sentry tools initialized with a codebase. + + Args: + codebase: The codebase to operate on + + Returns: + List of initialized Sentry tools + """ + return [ + ViewSentryIssuesTool(codebase), + ViewSentryIssueDetailsTool(codebase), + ViewSentryEventDetailsTool(codebase), + ViewSentryTool(codebase), + ] \ No newline at end of file diff --git a/src/codegen/extensions/langchain/tools.py b/src/codegen/extensions/langchain/tools.py index 877b59f05..bbe8e5938 100644 --- a/src/codegen/extensions/langchain/tools.py +++ b/src/codegen/extensions/langchain/tools.py @@ -25,6 +25,7 @@ from codegen.extensions.tools.search import search from codegen.extensions.tools.semantic_edit import semantic_edit from codegen.extensions.tools.semantic_search import semantic_search +from codegen.extensions.langchain.sentry_tools import get_sentry_tools from codegen.sdk.core.codebase import Codebase from ..tools import ( @@ -837,7 +838,7 @@ def get_workspace_tools(codebase: Codebase) -> list["BaseTool"]: Returns: List of initialized Langchain tools """ - return [ + tools = [ CommitTool(codebase), CreateFileTool(codebase), DeleteFileTool(codebase), @@ -869,6 +870,11 @@ def get_workspace_tools(codebase: Codebase) -> list["BaseTool"]: LinearCreateIssueTool(codebase), LinearGetTeamsTool(codebase), ] + + # Add Sentry tools + tools.extend(get_sentry_tools(codebase)) + + return tools class ReplacementEditInput(BaseModel): @@ -1023,4 +1029,4 @@ def _run( ) -> str: result = perform_reflection(context_summary=context_summary, findings_so_far=findings_so_far, current_challenges=current_challenges, reflection_focus=reflection_focus, codebase=self.codebase) - return result.render() + return result.render() \ No newline at end of file diff --git a/src/codegen/extensions/sentry/README.md b/src/codegen/extensions/sentry/README.md new file mode 100644 index 000000000..a4487daf4 --- /dev/null +++ b/src/codegen/extensions/sentry/README.md @@ -0,0 +1,138 @@ +# Sentry Integration for Codegen + +This module provides integration with Sentry, allowing you to view Sentry issues and events directly from Codegen. + +## Features + +- View Sentry issues across organizations and projects +- Filter issues by query parameters and status +- View detailed information about specific issues +- View events associated with issues +- View detailed information about specific events, including stack traces + +## Setup + +### Environment Variables + +The following environment variables need to be set: + +- `SENTRY_AUTH_TOKEN`: Your Sentry auth token +- `SENTRY_CODEGEN_INSTALLATION_UUID`: The installation UUID for the Codegen Sentry account +- `SENTRY_RAMP_INSTALLATION_UUID`: The installation UUID for the Ramp Sentry account (if applicable) + +### Usage + +#### Direct Usage + +```python +from codegen.extensions.tools.sentry.tools import ( + view_sentry_issues, + view_sentry_issue_details, + view_sentry_event_details, +) +from codegen.sdk.core.codebase import Codebase + +# Initialize a codebase +codebase = Codebase.from_local("./") + +# View issues +issues_result = view_sentry_issues( + codebase=codebase, + organization_slug="codegen-sh", + status="unresolved", + limit=5, +) +print(issues_result.render()) + +# View issue details +issue_result = view_sentry_issue_details( + codebase=codebase, + issue_id="ISSUE_ID", + organization_slug="codegen-sh", +) +print(issue_result.render()) + +# View event details +event_result = view_sentry_event_details( + codebase=codebase, + event_id="EVENT_ID", + organization_slug="codegen-sh", + project_slug="codegen", +) +print(event_result.render()) +``` + +#### Using LangChain Tools + +```python +from codegen.extensions.langchain.sentry_tools import ( + ViewSentryIssuesTool, + ViewSentryIssueDetailsTool, + ViewSentryEventDetailsTool, + ViewSentryTool, +) +from codegen.sdk.core.codebase import Codebase + +# Initialize a codebase +codebase = Codebase.from_local("./") + +# Initialize the tools +view_issues_tool = ViewSentryIssuesTool(codebase) +view_issue_tool = ViewSentryIssueDetailsTool(codebase) +view_event_tool = ViewSentryEventDetailsTool(codebase) +view_sentry_tool = ViewSentryTool(codebase) + +# Use the tools +result = view_sentry_tool._run( + action="view_issues", + organization_slug="codegen-sh", + status="unresolved", + limit=5, +) +print(result) +``` + +#### Using the Combined Tool + +The `ViewSentryTool` is a combined tool that can perform all Sentry operations: + +```python +from codegen.extensions.langchain.sentry_tools import ViewSentryTool +from codegen.sdk.core.codebase import Codebase + +# Initialize a codebase +codebase = Codebase.from_local("./") + +# Initialize the tool +view_sentry_tool = ViewSentryTool(codebase) + +# View issues +result = view_sentry_tool._run( + action="view_issues", + organization_slug="codegen-sh", + status="unresolved", + limit=5, +) +print(result) + +# View issue details +result = view_sentry_tool._run( + action="view_issue", + issue_id="ISSUE_ID", + organization_slug="codegen-sh", +) +print(result) + +# View event details +result = view_sentry_tool._run( + action="view_event", + event_id="EVENT_ID", + organization_slug="codegen-sh", + project_slug="codegen", +) +print(result) +``` + +## Example Notebook + +See the [example notebook](./example.ipynb) for a complete example of using the Sentry integration. \ No newline at end of file diff --git a/src/codegen/extensions/sentry/__init__.py b/src/codegen/extensions/sentry/__init__.py new file mode 100644 index 000000000..0882f0ab7 --- /dev/null +++ b/src/codegen/extensions/sentry/__init__.py @@ -0,0 +1 @@ +"""Sentry integration for Codegen.""" \ No newline at end of file diff --git a/src/codegen/extensions/sentry/config.py b/src/codegen/extensions/sentry/config.py new file mode 100644 index 000000000..fa38f1ebb --- /dev/null +++ b/src/codegen/extensions/sentry/config.py @@ -0,0 +1,56 @@ +"""Configuration for Sentry integration.""" + +import os +from typing import Dict, Optional + +# Environment variable names +SENTRY_AUTH_TOKEN_ENV = "SENTRY_AUTH_TOKEN" +SENTRY_CODEGEN_INSTALLATION_UUID_ENV = "SENTRY_CODEGEN_INSTALLATION_UUID" +SENTRY_RAMP_INSTALLATION_UUID_ENV = "SENTRY_RAMP_INSTALLATION_UUID" + +# Default values +DEFAULT_ORGANIZATION_SLUG = "codegen-sh" + + +def get_sentry_auth_token() -> Optional[str]: + """Get the Sentry auth token from environment variables. + + Returns: + The Sentry auth token, or None if not set. + """ + return os.environ.get(SENTRY_AUTH_TOKEN_ENV) + + +def get_installation_uuid(organization_slug: str) -> Optional[str]: + """Get the Sentry installation UUID for the specified organization. + + Args: + organization_slug: The organization slug to get the installation UUID for. + + Returns: + The Sentry installation UUID, or None if not set. + """ + if organization_slug == "codegen-sh": + return os.environ.get(SENTRY_CODEGEN_INSTALLATION_UUID_ENV) + elif organization_slug == "ramp": + return os.environ.get(SENTRY_RAMP_INSTALLATION_UUID_ENV) + return None + + +def get_available_organizations() -> Dict[str, Optional[str]]: + """Get a dictionary of available organizations and their installation UUIDs. + + Returns: + A dictionary mapping organization slugs to installation UUIDs. + """ + orgs = {} + + codegen_uuid = os.environ.get(SENTRY_CODEGEN_INSTALLATION_UUID_ENV) + if codegen_uuid: + orgs["codegen-sh"] = codegen_uuid + + ramp_uuid = os.environ.get(SENTRY_RAMP_INSTALLATION_UUID_ENV) + if ramp_uuid: + orgs["ramp"] = ramp_uuid + + return orgs \ No newline at end of file diff --git a/src/codegen/extensions/sentry/example.ipynb b/src/codegen/extensions/sentry/example.ipynb new file mode 100644 index 000000000..d0278e66e --- /dev/null +++ b/src/codegen/extensions/sentry/example.ipynb @@ -0,0 +1,226 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Sentry Integration Example\n", + "\n", + "This notebook demonstrates how to use the Sentry integration in Codegen." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First, let's import the necessary modules and set up the environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from codegen.extensions.sentry.sentry_client import SentryClient\n", + "from codegen.extensions.sentry.config import get_available_organizations\n", + "from codegen.extensions.tools.sentry.tools import (\n", + " view_sentry_issues,\n", + " view_sentry_issue_details,\n", + " view_sentry_event_details,\n", + ")\n", + "from codegen.sdk.core.codebase import Codebase" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Environment Variables\n", + "\n", + "Make sure you have the following environment variables set:\n", + "\n", + "- `SENTRY_AUTH_TOKEN`: Your Sentry auth token\n", + "- `SENTRY_CODEGEN_INSTALLATION_UUID`: The installation UUID for the Codegen Sentry account\n", + "- `SENTRY_RAMP_INSTALLATION_UUID`: The installation UUID for the Ramp Sentry account (if applicable)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check available organizations\n", + "available_orgs = get_available_organizations()\n", + "print(f\"Available organizations: {available_orgs}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initialize Codebase\n", + "\n", + "We need a Codebase instance to use the tools." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize a codebase (can be any codebase)\n", + "codebase = Codebase.from_local(\"./\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View Sentry Issues\n", + "\n", + "Let's view some Sentry issues for the Codegen organization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View unresolved issues for the Codegen organization\n", + "issues_result = view_sentry_issues(\n", + " codebase=codebase,\n", + " organization_slug=\"codegen-sh\",\n", + " status=\"unresolved\",\n", + " limit=5,\n", + ")\n", + "\n", + "print(issues_result.render())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View Issue Details\n", + "\n", + "Now, let's view details for a specific issue. Replace `ISSUE_ID` with an actual issue ID from the previous step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View details for a specific issue\n", + "# Replace ISSUE_ID with an actual issue ID\n", + "issue_id = \"ISSUE_ID\"\n", + "\n", + "issue_result = view_sentry_issue_details(\n", + " codebase=codebase,\n", + " issue_id=issue_id,\n", + " organization_slug=\"codegen-sh\",\n", + " limit=3,\n", + ")\n", + "\n", + "print(issue_result.render())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## View Event Details\n", + "\n", + "Finally, let's view details for a specific event. Replace `EVENT_ID` with an actual event ID from the previous step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# View details for a specific event\n", + "# Replace EVENT_ID with an actual event ID\n", + "event_id = \"EVENT_ID\"\n", + "\n", + "event_result = view_sentry_event_details(\n", + " codebase=codebase,\n", + " event_id=event_id,\n", + " organization_slug=\"codegen-sh\",\n", + " project_slug=\"codegen\",\n", + ")\n", + "\n", + "print(event_result.render())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the LangChain Tools\n", + "\n", + "You can also use the LangChain tools directly in an agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from codegen.extensions.langchain.sentry_tools import (\n", + " ViewSentryIssuesTool,\n", + " ViewSentryIssueDetailsTool,\n", + " ViewSentryEventDetailsTool,\n", + " ViewSentryTool,\n", + ")\n", + "\n", + "# Initialize the tools\n", + "view_issues_tool = ViewSentryIssuesTool(codebase)\n", + "view_issue_tool = ViewSentryIssueDetailsTool(codebase)\n", + "view_event_tool = ViewSentryEventDetailsTool(codebase)\n", + "view_sentry_tool = ViewSentryTool(codebase)\n", + "\n", + "# Example usage of the combined tool\n", + "result = view_sentry_tool._run(\n", + " action=\"view_issues\",\n", + " organization_slug=\"codegen-sh\",\n", + " status=\"unresolved\",\n", + " limit=5,\n", + ")\n", + "\n", + "print(result)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/src/codegen/extensions/sentry/sentry_client.py b/src/codegen/extensions/sentry/sentry_client.py new file mode 100644 index 000000000..7734a7416 --- /dev/null +++ b/src/codegen/extensions/sentry/sentry_client.py @@ -0,0 +1,232 @@ +"""Sentry API client for interacting with the Sentry API.""" + +import os +from typing import Any, Dict, List, Optional, Union + +import requests +from pydantic import BaseModel, Field + + +class SentryIssue(BaseModel): + """Sentry issue model.""" + + id: str = Field(..., description="Issue ID") + shortId: str = Field(..., description="Short ID (e.g., PROJECT-123)") + title: str = Field(..., description="Issue title") + culprit: str = Field(..., description="Culprit") + status: str = Field(..., description="Issue status") + level: str = Field(..., description="Issue level (e.g., error, warning)") + project: Dict[str, Any] = Field(..., description="Project information") + count: int = Field(..., description="Number of events") + userCount: int = Field(..., description="Number of users affected") + firstSeen: str = Field(..., description="First seen timestamp") + lastSeen: str = Field(..., description="Last seen timestamp") + permalink: str = Field(..., description="Permalink to the issue") + + +class SentryEvent(BaseModel): + """Sentry event model.""" + + id: str = Field(..., description="Event ID") + eventID: str = Field(..., description="Event ID") + title: str = Field(..., description="Event title") + message: Optional[str] = Field(None, description="Event message") + dateCreated: str = Field(..., description="Date created") + user: Optional[Dict[str, Any]] = Field(None, description="User information") + tags: List[Dict[str, str]] = Field(..., description="Event tags") + entries: List[Dict[str, Any]] = Field(..., description="Event entries") + contexts: Dict[str, Any] = Field(..., description="Event contexts") + sdk: Dict[str, Any] = Field(..., description="SDK information") + metadata: Dict[str, Any] = Field(..., description="Event metadata") + + +class SentryOrganization(BaseModel): + """Sentry organization model.""" + + id: str = Field(..., description="Organization ID") + slug: str = Field(..., description="Organization slug") + name: str = Field(..., description="Organization name") + dateCreated: str = Field(..., description="Date created") + isEarlyAdopter: bool = Field(..., description="Is early adopter") + require2FA: bool = Field(..., description="Requires 2FA") + status: Dict[str, Any] = Field(..., description="Organization status") + + +class SentryProject(BaseModel): + """Sentry project model.""" + + id: str = Field(..., description="Project ID") + slug: str = Field(..., description="Project slug") + name: str = Field(..., description="Project name") + platform: Optional[str] = Field(None, description="Project platform") + dateCreated: str = Field(..., description="Date created") + isBookmarked: bool = Field(..., description="Is bookmarked") + isMember: bool = Field(..., description="Is member") + features: List[str] = Field(..., description="Project features") + firstEvent: Optional[str] = Field(None, description="First event timestamp") + firstTransactionEvent: Optional[bool] = Field(None, description="Has first transaction event") + access: List[str] = Field(..., description="Access levels") + hasAccess: bool = Field(..., description="Has access") + hasMinifiedStackTrace: bool = Field(..., description="Has minified stack trace") + hasMonitors: bool = Field(..., description="Has monitors") + hasProfiles: bool = Field(..., description="Has profiles") + hasReplays: bool = Field(..., description="Has replays") + hasSessions: bool = Field(..., description="Has sessions") + isInternal: bool = Field(..., description="Is internal") + isPublic: bool = Field(..., description="Is public") + organization: Dict[str, Any] = Field(..., description="Organization information") + + +class SentryClient: + """Client for interacting with the Sentry API.""" + + def __init__(self, auth_token: Optional[str] = None, installation_uuid: Optional[str] = None): + """Initialize the Sentry API client. + + Args: + auth_token: Sentry auth token. If not provided, will look for SENTRY_AUTH_TOKEN env var. + installation_uuid: Sentry installation UUID. If not provided, will look for SENTRY_INSTALLATION_UUID env var. + """ + self.auth_token = auth_token or os.environ.get("SENTRY_AUTH_TOKEN") + self.installation_uuid = installation_uuid or os.environ.get("SENTRY_INSTALLATION_UUID") + + if not self.auth_token: + raise ValueError("Sentry auth token not provided. Set SENTRY_AUTH_TOKEN environment variable.") + + self.base_url = "https://sentry.io/api/0" + self.headers = { + "Authorization": f"Bearer {self.auth_token}", + "Content-Type": "application/json", + } + + def _make_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Make a request to the Sentry API. + + Args: + method: HTTP method (GET, POST, PUT, DELETE) + endpoint: API endpoint + params: Query parameters + data: Request body data + + Returns: + Response data as a dictionary + """ + url = f"{self.base_url}{endpoint}" + response = requests.request(method, url, headers=self.headers, params=params, json=data) + response.raise_for_status() + return response.json() + + def get_organizations(self) -> List[SentryOrganization]: + """Get a list of organizations the user has access to. + + Returns: + List of organizations + """ + data = self._make_request("GET", "/organizations/") + return [SentryOrganization(**org) for org in data] + + def get_projects(self, organization_slug: str) -> List[SentryProject]: + """Get a list of projects for an organization. + + Args: + organization_slug: Organization slug + + Returns: + List of projects + """ + data = self._make_request("GET", f"/organizations/{organization_slug}/projects/") + return [SentryProject(**project) for project in data] + + def get_issues( + self, + organization_slug: str, + project_slug: Optional[str] = None, + query: Optional[str] = None, + status: Optional[str] = None, + limit: int = 100, + cursor: Optional[str] = None, + ) -> Dict[str, Any]: + """Get a list of issues for an organization or project. + + Args: + organization_slug: Organization slug + project_slug: Optional project slug to filter by + query: Optional search query + status: Optional status filter (e.g., "resolved", "unresolved") + limit: Maximum number of issues to return + cursor: Pagination cursor + + Returns: + Dictionary containing issues and pagination info + """ + params = { + "limit": limit, + } + + if project_slug: + params["project"] = project_slug + + if query: + params["query"] = query + + if status: + params["status"] = status + + if cursor: + params["cursor"] = cursor + + return self._make_request("GET", f"/organizations/{organization_slug}/issues/", params=params) + + def get_issue_details(self, issue_id: str, organization_slug: str) -> SentryIssue: + """Get details for a specific issue. + + Args: + issue_id: Issue ID + organization_slug: Organization slug + + Returns: + Issue details + """ + data = self._make_request("GET", f"/organizations/{organization_slug}/issues/{issue_id}/") + return SentryIssue(**data) + + def get_issue_events( + self, + issue_id: str, + organization_slug: str, + limit: int = 100, + cursor: Optional[str] = None, + ) -> Dict[str, Any]: + """Get events for a specific issue. + + Args: + issue_id: Issue ID + organization_slug: Organization slug + limit: Maximum number of events to return + cursor: Pagination cursor + + Returns: + Dictionary containing events and pagination info + """ + params = { + "limit": limit, + } + + if cursor: + params["cursor"] = cursor + + return self._make_request("GET", f"/organizations/{organization_slug}/issues/{issue_id}/events/", params=params) + + def get_event_details(self, event_id: str, organization_slug: str, project_slug: str) -> SentryEvent: + """Get details for a specific event. + + Args: + event_id: Event ID + organization_slug: Organization slug + project_slug: Project slug + + Returns: + Event details + """ + data = self._make_request("GET", f"/projects/{organization_slug}/{project_slug}/events/{event_id}/") + return SentryEvent(**data) \ No newline at end of file diff --git a/src/codegen/extensions/tools/sentry/__init__.py b/src/codegen/extensions/tools/sentry/__init__.py new file mode 100644 index 000000000..259ac6250 --- /dev/null +++ b/src/codegen/extensions/tools/sentry/__init__.py @@ -0,0 +1 @@ +"""Sentry tools for Codegen.""" \ No newline at end of file diff --git a/src/codegen/extensions/tools/sentry/sentry_tool.py b/src/codegen/extensions/tools/sentry/sentry_tool.py new file mode 100644 index 000000000..858c4ee19 --- /dev/null +++ b/src/codegen/extensions/tools/sentry/sentry_tool.py @@ -0,0 +1,111 @@ +"""Sentry tool for viewing Sentry issues and events.""" + +from typing import Any, Dict, Optional + +from codegen.extensions.tools.sentry.tools import ( + view_sentry_event_details, + view_sentry_issue_details, + view_sentry_issues, +) +from codegen.sdk.core.codebase import Codebase + + +async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> Dict[str, Any]: + """Handle a Sentry tool call. + + Args: + codebase: The codebase to operate on + tool_call: The tool call parameters + + Returns: + The tool result + """ + action = tool_call.get("action", "view_issues") + args = tool_call.get("args", {}) + + if action == "view_issues": + organization_slug = args.get("organization_slug", "codegen-sh") + project_slug = args.get("project_slug") + query = args.get("query") + status = args.get("status", "unresolved") + limit = args.get("limit", 20) + cursor = args.get("cursor") + + result = view_sentry_issues( + codebase=codebase, + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + limit=limit, + cursor=cursor, + ) + + elif action == "view_issue": + issue_id = args.get("issue_id") + if not issue_id: + return { + "status": "error", + "error": "Missing required parameter: issue_id", + } + + organization_slug = args.get("organization_slug", "codegen-sh") + limit = args.get("limit", 10) + cursor = args.get("cursor") + + result = view_sentry_issue_details( + codebase=codebase, + issue_id=issue_id, + organization_slug=organization_slug, + limit=limit, + cursor=cursor, + ) + + elif action == "view_event": + event_id = args.get("event_id") + if not event_id: + return { + "status": "error", + "error": "Missing required parameter: event_id", + } + + organization_slug = args.get("organization_slug", "codegen-sh") + project_slug = args.get("project_slug", "codegen") + + result = view_sentry_event_details( + codebase=codebase, + event_id=event_id, + organization_slug=organization_slug, + project_slug=project_slug, + ) + + else: + return { + "status": "error", + "error": f"Unknown action: {action}. Supported actions: view_issues, view_issue, view_event", + } + + return { + "status": result.status, + "error": result.error if hasattr(result, "error") and result.error else None, + "result": result.render(), + } + + +# Export the tool for use in other modules +sentry_tool = { + "name": "ViewSentryTool", + "description": """View Sentry issues and events. + + This tool allows you to: + 1. View a list of Sentry issues for an organization or project + 2. View details of a specific Sentry issue, including its events + 3. View details of a specific Sentry event, including stack traces + + Examples: + - To view issues: {"action": "view_issues", "args": {"organization_slug": "codegen-sh", "project_slug": "codegen", "status": "unresolved"}} + - To view an issue: {"action": "view_issue", "args": {"issue_id": "123456", "organization_slug": "codegen-sh"}} + - To view an event: {"action": "view_event", "args": {"event_id": "abcdef1234567890", "organization_slug": "codegen-sh", "project_slug": "codegen"}} + """, + "handler": handle_sentry_tool, +} \ No newline at end of file diff --git a/src/codegen/extensions/tools/sentry/tools.py b/src/codegen/extensions/tools/sentry/tools.py new file mode 100644 index 000000000..3332824cf --- /dev/null +++ b/src/codegen/extensions/tools/sentry/tools.py @@ -0,0 +1,467 @@ +"""Tools for interacting with Sentry.""" + +from typing import Any, ClassVar, Dict, List, Optional + +from pydantic import Field + +from codegen.extensions.sentry.config import ( + DEFAULT_ORGANIZATION_SLUG, + get_available_organizations, + get_installation_uuid, + get_sentry_auth_token, +) +from codegen.extensions.sentry.sentry_client import SentryClient +from codegen.sdk.core.codebase import Codebase + +from ..observation import Observation + + +class SentryIssuesObservation(Observation): + """Response from viewing Sentry issues.""" + + organization_slug: str = Field(description="Organization slug") + project_slug: Optional[str] = Field(None, description="Project slug (if specified)") + query: Optional[str] = Field(None, description="Search query (if specified)") + status: str = Field(description="Status filter (if specified)") + issues: List[Dict[str, Any]] = Field(description="List of issues") + has_more: bool = Field(description="Whether there are more issues to fetch") + next_cursor: Optional[str] = Field(None, description="Cursor for fetching the next page") + + str_template: ClassVar[str] = "Found {issue_count} issues in {organization_slug}" + + def _get_details(self) -> Dict[str, Any]: + """Get details for string representation.""" + return { + "issue_count": len(self.issues), + "organization_slug": self.organization_slug, + } + + def render(self) -> str: + """Render the issues view.""" + header = f"[SENTRY ISSUES]: Organization: {self.organization_slug}" + + if self.project_slug: + header += f", Project: {self.project_slug}" + + if self.query: + header += f", Query: '{self.query}'" + + if self.status: + header += f", Status: {self.status}" + + header += f"\nFound {len(self.issues)} issues" + + if self.has_more: + header += " (more available)" + + if not self.issues: + return f"{header}\n\nNo issues found." + + issues_text = "" + for issue in self.issues: + issues_text += f"\n\n## {issue.get('shortId', 'Unknown')} - {issue.get('title', 'Untitled')}" + issues_text += f"\nStatus: {issue.get('status', 'Unknown')}" + issues_text += f"\nLevel: {issue.get('level', 'Unknown')}" + issues_text += f"\nEvents: {issue.get('count', 0)}" + issues_text += f"\nUsers affected: {issue.get('userCount', 0)}" + issues_text += f"\nFirst seen: {issue.get('firstSeen', 'Unknown')}" + issues_text += f"\nLast seen: {issue.get('lastSeen', 'Unknown')}" + issues_text += f"\nPermalink: {issue.get('permalink', 'Unknown')}" + + return f"{header}{issues_text}" + + +class SentryIssueDetailsObservation(Observation): + """Response from viewing a specific Sentry issue.""" + + organization_slug: str = Field(description="Organization slug") + issue_id: str = Field(description="Issue ID") + issue_data: Dict[str, Any] = Field(description="Issue data") + events: List[Dict[str, Any]] = Field(description="List of events for this issue") + has_more_events: bool = Field(description="Whether there are more events to fetch") + next_cursor: Optional[str] = Field(None, description="Cursor for fetching the next page of events") + + str_template: ClassVar[str] = "Issue {issue_id} with {event_count} events" + + def _get_details(self) -> Dict[str, Any]: + """Get details for string representation.""" + return { + "issue_id": self.issue_id, + "event_count": len(self.events), + } + + def render(self) -> str: + """Render the issue details view.""" + issue = self.issue_data + + header = f"[SENTRY ISSUE]: {issue.get('shortId', 'Unknown')} - {issue.get('title', 'Untitled')}" + header += f"\nOrganization: {self.organization_slug}" + + details = f"\nStatus: {issue.get('status', 'Unknown')}" + details += f"\nLevel: {issue.get('level', 'Unknown')}" + details += f"\nEvents: {issue.get('count', 0)}" + details += f"\nUsers affected: {issue.get('userCount', 0)}" + details += f"\nFirst seen: {issue.get('firstSeen', 'Unknown')}" + details += f"\nLast seen: {issue.get('lastSeen', 'Unknown')}" + details += f"\nPermalink: {issue.get('permalink', 'Unknown')}" + + events_header = f"\n\n## Events ({len(self.events)})" + if self.has_more_events: + events_header += " (more available)" + + events_text = "" + for event in self.events: + events_text += f"\n\n### Event {event.get('eventID', 'Unknown')}" + events_text += f"\nTimestamp: {event.get('dateCreated', 'Unknown')}" + + # Add user info if available + user = event.get('user') + if user: + events_text += f"\nUser: {user.get('username') or user.get('email') or user.get('ip_address') or 'Anonymous'}" + + # Add tags + tags = event.get('tags', []) + if tags: + events_text += "\nTags:" + for tag in tags: + events_text += f"\n - {tag.get('key', '')}: {tag.get('value', '')}" + + return f"{header}{details}{events_header}{events_text}" + + +class SentryEventDetailsObservation(Observation): + """Response from viewing a specific Sentry event.""" + + organization_slug: str = Field(description="Organization slug") + project_slug: str = Field(description="Project slug") + event_id: str = Field(description="Event ID") + event_data: Dict[str, Any] = Field(description="Event data") + + str_template: ClassVar[str] = "Event {event_id}" + + def render(self) -> str: + """Render the event details view.""" + event = self.event_data + + header = f"[SENTRY EVENT]: {event.get('eventID', 'Unknown')}" + header += f"\nOrganization: {self.organization_slug}" + header += f"\nProject: {self.project_slug}" + header += f"\nTitle: {event.get('title', 'Untitled')}" + + details = f"\nTimestamp: {event.get('dateCreated', 'Unknown')}" + + # Add user info if available + user = event.get('user') + if user: + details += f"\nUser: {user.get('username') or user.get('email') or user.get('ip_address') or 'Anonymous'}" + + # Add tags + tags = event.get('tags', []) + if tags: + details += "\nTags:" + for tag in tags: + details += f"\n - {tag.get('key', '')}: {tag.get('value', '')}" + + # Add exception info if available + exception_entries = [entry for entry in event.get('entries', []) if entry.get('type') == 'exception'] + if exception_entries: + details += "\n\n## Exception" + for entry in exception_entries: + for exception in entry.get('data', {}).get('values', []): + details += f"\n\n### {exception.get('type', 'Unknown')}: {exception.get('value', 'Unknown')}" + + # Add stack trace + frames = exception.get('stacktrace', {}).get('frames', []) + if frames: + details += "\n\nStack Trace:" + for frame in frames: + filename = frame.get('filename', frame.get('abs_path', 'Unknown')) + function = frame.get('function', 'Unknown') + lineno = frame.get('lineno', '?') + details += f"\n - {filename}:{lineno} in {function}" + + # Add context if available + context_line = frame.get('context_line') + if context_line: + details += f"\n {context_line.strip()}" + + return f"{header}{details}" + + +def view_sentry_issues( + codebase: Codebase, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + project_slug: Optional[str] = None, + query: Optional[str] = None, + status: str = "unresolved", + limit: int = 20, + cursor: Optional[str] = None, +) -> SentryIssuesObservation: + """View Sentry issues for an organization or project. + + Args: + codebase: The codebase to operate on + organization_slug: Organization slug + project_slug: Optional project slug to filter by + query: Optional search query + status: Status filter (e.g., "resolved", "unresolved") + limit: Maximum number of issues to return + cursor: Pagination cursor + """ + try: + # Get auth token and installation UUID + auth_token = get_sentry_auth_token() + installation_uuid = get_installation_uuid(organization_slug) + + if not auth_token: + return SentryIssuesObservation( + status="error", + error="Sentry auth token not found. Please set the SENTRY_AUTH_TOKEN environment variable.", + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + issues=[], + has_more=False, + ) + + if not installation_uuid: + available_orgs = get_available_organizations() + if not available_orgs: + org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." + else: + org_message = f"Available organizations: {', '.join(available_orgs.keys())}" + + return SentryIssuesObservation( + status="error", + error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + issues=[], + has_more=False, + ) + + # Initialize Sentry client + client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) + + # Get issues + response = client.get_issues( + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + limit=limit, + cursor=cursor, + ) + + # Extract pagination info + has_more = False + next_cursor = None + + if isinstance(response, dict): + issues = response.get('data', []) + + # Check for pagination info + pagination = response.get('pagination', {}) + has_more = pagination.get('hasMore', False) + next_cursor = pagination.get('nextCursor') + else: + issues = response + + return SentryIssuesObservation( + status="success", + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + issues=issues, + has_more=has_more, + next_cursor=next_cursor, + ) + + except Exception as e: + return SentryIssuesObservation( + status="error", + error=f"Failed to view Sentry issues: {e!s}", + organization_slug=organization_slug, + project_slug=project_slug, + query=query, + status=status, + issues=[], + has_more=False, + ) + + +def view_sentry_issue_details( + codebase: Codebase, + issue_id: str, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + limit: int = 10, + cursor: Optional[str] = None, +) -> SentryIssueDetailsObservation: + """View details for a specific Sentry issue. + + Args: + codebase: The codebase to operate on + issue_id: Issue ID + organization_slug: Organization slug + limit: Maximum number of events to return + cursor: Pagination cursor + """ + try: + # Get auth token and installation UUID + auth_token = get_sentry_auth_token() + installation_uuid = get_installation_uuid(organization_slug) + + if not auth_token: + return SentryIssueDetailsObservation( + status="error", + error="Sentry auth token not found. Please set the SENTRY_AUTH_TOKEN environment variable.", + organization_slug=organization_slug, + issue_id=issue_id, + issue_data={}, + events=[], + has_more_events=False, + ) + + if not installation_uuid: + available_orgs = get_available_organizations() + if not available_orgs: + org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." + else: + org_message = f"Available organizations: {', '.join(available_orgs.keys())}" + + return SentryIssueDetailsObservation( + status="error", + error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", + organization_slug=organization_slug, + issue_id=issue_id, + issue_data={}, + events=[], + has_more_events=False, + ) + + # Initialize Sentry client + client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) + + # Get issue details + issue = client.get_issue_details(issue_id=issue_id, organization_slug=organization_slug) + + # Get events for the issue + events_response = client.get_issue_events( + issue_id=issue_id, + organization_slug=organization_slug, + limit=limit, + cursor=cursor, + ) + + # Extract pagination info + has_more = False + next_cursor = None + + if isinstance(events_response, dict): + events = events_response.get('data', []) + + # Check for pagination info + pagination = events_response.get('pagination', {}) + has_more = pagination.get('hasMore', False) + next_cursor = pagination.get('nextCursor') + else: + events = events_response + + return SentryIssueDetailsObservation( + status="success", + organization_slug=organization_slug, + issue_id=issue_id, + issue_data=issue.dict() if hasattr(issue, 'dict') else issue, + events=events, + has_more_events=has_more, + next_cursor=next_cursor, + ) + + except Exception as e: + return SentryIssueDetailsObservation( + status="error", + error=f"Failed to view Sentry issue details: {e!s}", + organization_slug=organization_slug, + issue_id=issue_id, + issue_data={}, + events=[], + has_more_events=False, + ) + + +def view_sentry_event_details( + codebase: Codebase, + event_id: str, + organization_slug: str = DEFAULT_ORGANIZATION_SLUG, + project_slug: str = "codegen", +) -> SentryEventDetailsObservation: + """View details for a specific Sentry event. + + Args: + codebase: The codebase to operate on + event_id: Event ID + organization_slug: Organization slug + project_slug: Project slug + """ + try: + # Get auth token and installation UUID + auth_token = get_sentry_auth_token() + installation_uuid = get_installation_uuid(organization_slug) + + if not auth_token: + return SentryEventDetailsObservation( + status="error", + error="Sentry auth token not found. Please set the SENTRY_AUTH_TOKEN environment variable.", + organization_slug=organization_slug, + project_slug=project_slug, + event_id=event_id, + event_data={}, + ) + + if not installation_uuid: + available_orgs = get_available_organizations() + if not available_orgs: + org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." + else: + org_message = f"Available organizations: {', '.join(available_orgs.keys())}" + + return SentryEventDetailsObservation( + status="error", + error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", + organization_slug=organization_slug, + project_slug=project_slug, + event_id=event_id, + event_data={}, + ) + + # Initialize Sentry client + client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) + + # Get event details + event = client.get_event_details( + event_id=event_id, + organization_slug=organization_slug, + project_slug=project_slug, + ) + + return SentryEventDetailsObservation( + status="success", + organization_slug=organization_slug, + project_slug=project_slug, + event_id=event_id, + event_data=event.dict() if hasattr(event, 'dict') else event, + ) + + except Exception as e: + return SentryEventDetailsObservation( + status="error", + error=f"Failed to view Sentry event details: {e!s}", + organization_slug=organization_slug, + project_slug=project_slug, + event_id=event_id, + event_data={}, + ) \ No newline at end of file From 1ea5b0013ed19a532a0c36c5beadd9885be7626c Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 18:39:20 +0000 Subject: [PATCH 2/2] Automated pre-commit update --- .../extensions/langchain/sentry_tools.py | 24 ++-- src/codegen/extensions/langchain/tools.py | 8 +- src/codegen/extensions/sentry/README.md | 2 +- src/codegen/extensions/sentry/__init__.py | 2 +- src/codegen/extensions/sentry/config.py | 12 +- src/codegen/extensions/sentry/example.ipynb | 2 +- .../extensions/sentry/sentry_client.py | 61 +++++----- .../extensions/tools/sentry/__init__.py | 2 +- .../extensions/tools/sentry/sentry_tool.py | 30 ++--- src/codegen/extensions/tools/sentry/tools.py | 104 +++++++++--------- 10 files changed, 124 insertions(+), 123 deletions(-) diff --git a/src/codegen/extensions/langchain/sentry_tools.py b/src/codegen/extensions/langchain/sentry_tools.py index 00f91cab8..7dfe46c81 100644 --- a/src/codegen/extensions/langchain/sentry_tools.py +++ b/src/codegen/extensions/langchain/sentry_tools.py @@ -1,6 +1,6 @@ """LangChain tools for Sentry integration.""" -from typing import ClassVar, Dict, List, Optional +from typing import ClassVar, Optional from langchain_core.tools.base import BaseTool from pydantic import BaseModel, Field @@ -87,7 +87,7 @@ class ViewSentryIssuesTool(BaseTool): name: ClassVar[str] = "view_sentry_issues" description: ClassVar[str] = """ View a list of Sentry issues for an organization or project. - + This tool allows you to retrieve and filter Sentry issues by organization, project, status, and search query. Results are paginated, and you can use the cursor parameter to fetch additional pages. """ @@ -124,7 +124,7 @@ class ViewSentryIssueDetailsTool(BaseTool): name: ClassVar[str] = "view_sentry_issue" description: ClassVar[str] = """ View detailed information about a specific Sentry issue, including its events. - + This tool retrieves comprehensive information about a Sentry issue, including its metadata and associated events. Events are paginated, and you can use the cursor parameter to fetch additional pages. """ @@ -157,7 +157,7 @@ class ViewSentryEventDetailsTool(BaseTool): name: ClassVar[str] = "view_sentry_event" description: ClassVar[str] = """ View detailed information about a specific Sentry event. - + This tool retrieves comprehensive information about a Sentry event, including its metadata, tags, user information, and stack trace (if available). """ @@ -229,12 +229,12 @@ class ViewSentryTool(BaseTool): name: ClassVar[str] = "view_sentry" description: ClassVar[str] = """ View Sentry issues and events. - + This tool allows you to: 1. View a list of Sentry issues for an organization or project 2. View details of a specific Sentry issue, including its events 3. View details of a specific Sentry event, including stack traces - + Specify the action parameter to choose which operation to perform: - 'view_issues': List issues (default) - 'view_issue': View a specific issue (requires issue_id) @@ -271,7 +271,7 @@ def _run( elif action == "view_issue": if not issue_id: return "Error: Missing required parameter 'issue_id' for action 'view_issue'" - + result = view_sentry_issue_details( codebase=self.codebase, issue_id=issue_id, @@ -282,10 +282,10 @@ def _run( elif action == "view_event": if not event_id: return "Error: Missing required parameter 'event_id' for action 'view_event'" - + if not project_slug: return "Error: Missing required parameter 'project_slug' for action 'view_event'" - + result = view_sentry_event_details( codebase=self.codebase, event_id=event_id, @@ -294,11 +294,11 @@ def _run( ) else: return f"Error: Unknown action '{action}'. Supported actions: 'view_issues', 'view_issue', 'view_event'" - + return result.render() -def get_sentry_tools(codebase: Codebase) -> List[BaseTool]: +def get_sentry_tools(codebase: Codebase) -> list[BaseTool]: """Get all Sentry tools initialized with a codebase. Args: @@ -312,4 +312,4 @@ def get_sentry_tools(codebase: Codebase) -> List[BaseTool]: ViewSentryIssueDetailsTool(codebase), ViewSentryEventDetailsTool(codebase), ViewSentryTool(codebase), - ] \ No newline at end of file + ] diff --git a/src/codegen/extensions/langchain/tools.py b/src/codegen/extensions/langchain/tools.py index bbe8e5938..3596f63a5 100644 --- a/src/codegen/extensions/langchain/tools.py +++ b/src/codegen/extensions/langchain/tools.py @@ -5,6 +5,7 @@ from langchain_core.tools.base import BaseTool from pydantic import BaseModel, Field +from codegen.extensions.langchain.sentry_tools import get_sentry_tools from codegen.extensions.linear.linear_client import LinearClient from codegen.extensions.tools.bash import run_bash_command from codegen.extensions.tools.github.checkout_pr import checkout_pr @@ -25,7 +26,6 @@ from codegen.extensions.tools.search import search from codegen.extensions.tools.semantic_edit import semantic_edit from codegen.extensions.tools.semantic_search import semantic_search -from codegen.extensions.langchain.sentry_tools import get_sentry_tools from codegen.sdk.core.codebase import Codebase from ..tools import ( @@ -870,10 +870,10 @@ def get_workspace_tools(codebase: Codebase) -> list["BaseTool"]: LinearCreateIssueTool(codebase), LinearGetTeamsTool(codebase), ] - + # Add Sentry tools tools.extend(get_sentry_tools(codebase)) - + return tools @@ -1029,4 +1029,4 @@ def _run( ) -> str: result = perform_reflection(context_summary=context_summary, findings_so_far=findings_so_far, current_challenges=current_challenges, reflection_focus=reflection_focus, codebase=self.codebase) - return result.render() \ No newline at end of file + return result.render() diff --git a/src/codegen/extensions/sentry/README.md b/src/codegen/extensions/sentry/README.md index a4487daf4..eded30599 100644 --- a/src/codegen/extensions/sentry/README.md +++ b/src/codegen/extensions/sentry/README.md @@ -135,4 +135,4 @@ print(result) ## Example Notebook -See the [example notebook](./example.ipynb) for a complete example of using the Sentry integration. \ No newline at end of file +See the [example notebook](./example.ipynb) for a complete example of using the Sentry integration. diff --git a/src/codegen/extensions/sentry/__init__.py b/src/codegen/extensions/sentry/__init__.py index 0882f0ab7..d9d888cda 100644 --- a/src/codegen/extensions/sentry/__init__.py +++ b/src/codegen/extensions/sentry/__init__.py @@ -1 +1 @@ -"""Sentry integration for Codegen.""" \ No newline at end of file +"""Sentry integration for Codegen.""" diff --git a/src/codegen/extensions/sentry/config.py b/src/codegen/extensions/sentry/config.py index fa38f1ebb..7cb058e6d 100644 --- a/src/codegen/extensions/sentry/config.py +++ b/src/codegen/extensions/sentry/config.py @@ -1,7 +1,7 @@ """Configuration for Sentry integration.""" import os -from typing import Dict, Optional +from typing import Optional # Environment variable names SENTRY_AUTH_TOKEN_ENV = "SENTRY_AUTH_TOKEN" @@ -37,20 +37,20 @@ def get_installation_uuid(organization_slug: str) -> Optional[str]: return None -def get_available_organizations() -> Dict[str, Optional[str]]: +def get_available_organizations() -> dict[str, Optional[str]]: """Get a dictionary of available organizations and their installation UUIDs. Returns: A dictionary mapping organization slugs to installation UUIDs. """ orgs = {} - + codegen_uuid = os.environ.get(SENTRY_CODEGEN_INSTALLATION_UUID_ENV) if codegen_uuid: orgs["codegen-sh"] = codegen_uuid - + ramp_uuid = os.environ.get(SENTRY_RAMP_INSTALLATION_UUID_ENV) if ramp_uuid: orgs["ramp"] = ramp_uuid - - return orgs \ No newline at end of file + + return orgs diff --git a/src/codegen/extensions/sentry/example.ipynb b/src/codegen/extensions/sentry/example.ipynb index d0278e66e..01d4ba871 100644 --- a/src/codegen/extensions/sentry/example.ipynb +++ b/src/codegen/extensions/sentry/example.ipynb @@ -223,4 +223,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/src/codegen/extensions/sentry/sentry_client.py b/src/codegen/extensions/sentry/sentry_client.py index 7734a7416..b773e3190 100644 --- a/src/codegen/extensions/sentry/sentry_client.py +++ b/src/codegen/extensions/sentry/sentry_client.py @@ -1,7 +1,7 @@ """Sentry API client for interacting with the Sentry API.""" import os -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional import requests from pydantic import BaseModel, Field @@ -16,7 +16,7 @@ class SentryIssue(BaseModel): culprit: str = Field(..., description="Culprit") status: str = Field(..., description="Issue status") level: str = Field(..., description="Issue level (e.g., error, warning)") - project: Dict[str, Any] = Field(..., description="Project information") + project: dict[str, Any] = Field(..., description="Project information") count: int = Field(..., description="Number of events") userCount: int = Field(..., description="Number of users affected") firstSeen: str = Field(..., description="First seen timestamp") @@ -32,12 +32,12 @@ class SentryEvent(BaseModel): title: str = Field(..., description="Event title") message: Optional[str] = Field(None, description="Event message") dateCreated: str = Field(..., description="Date created") - user: Optional[Dict[str, Any]] = Field(None, description="User information") - tags: List[Dict[str, str]] = Field(..., description="Event tags") - entries: List[Dict[str, Any]] = Field(..., description="Event entries") - contexts: Dict[str, Any] = Field(..., description="Event contexts") - sdk: Dict[str, Any] = Field(..., description="SDK information") - metadata: Dict[str, Any] = Field(..., description="Event metadata") + user: Optional[dict[str, Any]] = Field(None, description="User information") + tags: list[dict[str, str]] = Field(..., description="Event tags") + entries: list[dict[str, Any]] = Field(..., description="Event entries") + contexts: dict[str, Any] = Field(..., description="Event contexts") + sdk: dict[str, Any] = Field(..., description="SDK information") + metadata: dict[str, Any] = Field(..., description="Event metadata") class SentryOrganization(BaseModel): @@ -49,7 +49,7 @@ class SentryOrganization(BaseModel): dateCreated: str = Field(..., description="Date created") isEarlyAdopter: bool = Field(..., description="Is early adopter") require2FA: bool = Field(..., description="Requires 2FA") - status: Dict[str, Any] = Field(..., description="Organization status") + status: dict[str, Any] = Field(..., description="Organization status") class SentryProject(BaseModel): @@ -62,10 +62,10 @@ class SentryProject(BaseModel): dateCreated: str = Field(..., description="Date created") isBookmarked: bool = Field(..., description="Is bookmarked") isMember: bool = Field(..., description="Is member") - features: List[str] = Field(..., description="Project features") + features: list[str] = Field(..., description="Project features") firstEvent: Optional[str] = Field(None, description="First event timestamp") firstTransactionEvent: Optional[bool] = Field(None, description="Has first transaction event") - access: List[str] = Field(..., description="Access levels") + access: list[str] = Field(..., description="Access levels") hasAccess: bool = Field(..., description="Has access") hasMinifiedStackTrace: bool = Field(..., description="Has minified stack trace") hasMonitors: bool = Field(..., description="Has monitors") @@ -74,7 +74,7 @@ class SentryProject(BaseModel): hasSessions: bool = Field(..., description="Has sessions") isInternal: bool = Field(..., description="Is internal") isPublic: bool = Field(..., description="Is public") - organization: Dict[str, Any] = Field(..., description="Organization information") + organization: dict[str, Any] = Field(..., description="Organization information") class SentryClient: @@ -89,17 +89,18 @@ def __init__(self, auth_token: Optional[str] = None, installation_uuid: Optional """ self.auth_token = auth_token or os.environ.get("SENTRY_AUTH_TOKEN") self.installation_uuid = installation_uuid or os.environ.get("SENTRY_INSTALLATION_UUID") - + if not self.auth_token: - raise ValueError("Sentry auth token not provided. Set SENTRY_AUTH_TOKEN environment variable.") - + msg = "Sentry auth token not provided. Set SENTRY_AUTH_TOKEN environment variable." + raise ValueError(msg) + self.base_url = "https://sentry.io/api/0" self.headers = { "Authorization": f"Bearer {self.auth_token}", "Content-Type": "application/json", } - def _make_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def _make_request(self, method: str, endpoint: str, params: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None) -> dict[str, Any]: """Make a request to the Sentry API. Args: @@ -116,7 +117,7 @@ def _make_request(self, method: str, endpoint: str, params: Optional[Dict[str, A response.raise_for_status() return response.json() - def get_organizations(self) -> List[SentryOrganization]: + def get_organizations(self) -> list[SentryOrganization]: """Get a list of organizations the user has access to. Returns: @@ -125,7 +126,7 @@ def get_organizations(self) -> List[SentryOrganization]: data = self._make_request("GET", "/organizations/") return [SentryOrganization(**org) for org in data] - def get_projects(self, organization_slug: str) -> List[SentryProject]: + def get_projects(self, organization_slug: str) -> list[SentryProject]: """Get a list of projects for an organization. Args: @@ -138,14 +139,14 @@ def get_projects(self, organization_slug: str) -> List[SentryProject]: return [SentryProject(**project) for project in data] def get_issues( - self, - organization_slug: str, + self, + organization_slug: str, project_slug: Optional[str] = None, query: Optional[str] = None, status: Optional[str] = None, limit: int = 100, cursor: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get a list of issues for an organization or project. Args: @@ -162,16 +163,16 @@ def get_issues( params = { "limit": limit, } - + if project_slug: params["project"] = project_slug - + if query: params["query"] = query - + if status: params["status"] = status - + if cursor: params["cursor"] = cursor @@ -191,12 +192,12 @@ def get_issue_details(self, issue_id: str, organization_slug: str) -> SentryIssu return SentryIssue(**data) def get_issue_events( - self, - issue_id: str, + self, + issue_id: str, organization_slug: str, limit: int = 100, cursor: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get events for a specific issue. Args: @@ -211,7 +212,7 @@ def get_issue_events( params = { "limit": limit, } - + if cursor: params["cursor"] = cursor @@ -229,4 +230,4 @@ def get_event_details(self, event_id: str, organization_slug: str, project_slug: Event details """ data = self._make_request("GET", f"/projects/{organization_slug}/{project_slug}/events/{event_id}/") - return SentryEvent(**data) \ No newline at end of file + return SentryEvent(**data) diff --git a/src/codegen/extensions/tools/sentry/__init__.py b/src/codegen/extensions/tools/sentry/__init__.py index 259ac6250..9fd490917 100644 --- a/src/codegen/extensions/tools/sentry/__init__.py +++ b/src/codegen/extensions/tools/sentry/__init__.py @@ -1 +1 @@ -"""Sentry tools for Codegen.""" \ No newline at end of file +"""Sentry tools for Codegen.""" diff --git a/src/codegen/extensions/tools/sentry/sentry_tool.py b/src/codegen/extensions/tools/sentry/sentry_tool.py index 858c4ee19..397d98c2e 100644 --- a/src/codegen/extensions/tools/sentry/sentry_tool.py +++ b/src/codegen/extensions/tools/sentry/sentry_tool.py @@ -1,6 +1,6 @@ """Sentry tool for viewing Sentry issues and events.""" -from typing import Any, Dict, Optional +from typing import Any from codegen.extensions.tools.sentry.tools import ( view_sentry_event_details, @@ -10,7 +10,7 @@ from codegen.sdk.core.codebase import Codebase -async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> Dict[str, Any]: +async def handle_sentry_tool(codebase: Codebase, tool_call: dict[str, Any]) -> dict[str, Any]: """Handle a Sentry tool call. Args: @@ -22,7 +22,7 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D """ action = tool_call.get("action", "view_issues") args = tool_call.get("args", {}) - + if action == "view_issues": organization_slug = args.get("organization_slug", "codegen-sh") project_slug = args.get("project_slug") @@ -30,7 +30,7 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D status = args.get("status", "unresolved") limit = args.get("limit", 20) cursor = args.get("cursor") - + result = view_sentry_issues( codebase=codebase, organization_slug=organization_slug, @@ -40,7 +40,7 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D limit=limit, cursor=cursor, ) - + elif action == "view_issue": issue_id = args.get("issue_id") if not issue_id: @@ -48,11 +48,11 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D "status": "error", "error": "Missing required parameter: issue_id", } - + organization_slug = args.get("organization_slug", "codegen-sh") limit = args.get("limit", 10) cursor = args.get("cursor") - + result = view_sentry_issue_details( codebase=codebase, issue_id=issue_id, @@ -60,7 +60,7 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D limit=limit, cursor=cursor, ) - + elif action == "view_event": event_id = args.get("event_id") if not event_id: @@ -68,23 +68,23 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D "status": "error", "error": "Missing required parameter: event_id", } - + organization_slug = args.get("organization_slug", "codegen-sh") project_slug = args.get("project_slug", "codegen") - + result = view_sentry_event_details( codebase=codebase, event_id=event_id, organization_slug=organization_slug, project_slug=project_slug, ) - + else: return { "status": "error", "error": f"Unknown action: {action}. Supported actions: view_issues, view_issue, view_event", } - + return { "status": result.status, "error": result.error if hasattr(result, "error") and result.error else None, @@ -96,16 +96,16 @@ async def handle_sentry_tool(codebase: Codebase, tool_call: Dict[str, Any]) -> D sentry_tool = { "name": "ViewSentryTool", "description": """View Sentry issues and events. - + This tool allows you to: 1. View a list of Sentry issues for an organization or project 2. View details of a specific Sentry issue, including its events 3. View details of a specific Sentry event, including stack traces - + Examples: - To view issues: {"action": "view_issues", "args": {"organization_slug": "codegen-sh", "project_slug": "codegen", "status": "unresolved"}} - To view an issue: {"action": "view_issue", "args": {"issue_id": "123456", "organization_slug": "codegen-sh"}} - To view an event: {"action": "view_event", "args": {"event_id": "abcdef1234567890", "organization_slug": "codegen-sh", "project_slug": "codegen"}} """, "handler": handle_sentry_tool, -} \ No newline at end of file +} diff --git a/src/codegen/extensions/tools/sentry/tools.py b/src/codegen/extensions/tools/sentry/tools.py index 3332824cf..307b20942 100644 --- a/src/codegen/extensions/tools/sentry/tools.py +++ b/src/codegen/extensions/tools/sentry/tools.py @@ -39,24 +39,24 @@ def _get_details(self) -> Dict[str, Any]: def render(self) -> str: """Render the issues view.""" header = f"[SENTRY ISSUES]: Organization: {self.organization_slug}" - + if self.project_slug: header += f", Project: {self.project_slug}" - + if self.query: header += f", Query: '{self.query}'" - + if self.status: header += f", Status: {self.status}" - + header += f"\nFound {len(self.issues)} issues" - + if self.has_more: header += " (more available)" - + if not self.issues: return f"{header}\n\nNo issues found." - + issues_text = "" for issue in self.issues: issues_text += f"\n\n## {issue.get('shortId', 'Unknown')} - {issue.get('title', 'Untitled')}" @@ -67,7 +67,7 @@ def render(self) -> str: issues_text += f"\nFirst seen: {issue.get('firstSeen', 'Unknown')}" issues_text += f"\nLast seen: {issue.get('lastSeen', 'Unknown')}" issues_text += f"\nPermalink: {issue.get('permalink', 'Unknown')}" - + return f"{header}{issues_text}" @@ -93,10 +93,10 @@ def _get_details(self) -> Dict[str, Any]: def render(self) -> str: """Render the issue details view.""" issue = self.issue_data - + header = f"[SENTRY ISSUE]: {issue.get('shortId', 'Unknown')} - {issue.get('title', 'Untitled')}" header += f"\nOrganization: {self.organization_slug}" - + details = f"\nStatus: {issue.get('status', 'Unknown')}" details += f"\nLevel: {issue.get('level', 'Unknown')}" details += f"\nEvents: {issue.get('count', 0)}" @@ -104,28 +104,28 @@ def render(self) -> str: details += f"\nFirst seen: {issue.get('firstSeen', 'Unknown')}" details += f"\nLast seen: {issue.get('lastSeen', 'Unknown')}" details += f"\nPermalink: {issue.get('permalink', 'Unknown')}" - + events_header = f"\n\n## Events ({len(self.events)})" if self.has_more_events: events_header += " (more available)" - + events_text = "" for event in self.events: events_text += f"\n\n### Event {event.get('eventID', 'Unknown')}" events_text += f"\nTimestamp: {event.get('dateCreated', 'Unknown')}" - + # Add user info if available user = event.get('user') if user: events_text += f"\nUser: {user.get('username') or user.get('email') or user.get('ip_address') or 'Anonymous'}" - + # Add tags tags = event.get('tags', []) if tags: events_text += "\nTags:" for tag in tags: events_text += f"\n - {tag.get('key', '')}: {tag.get('value', '')}" - + return f"{header}{details}{events_header}{events_text}" @@ -142,26 +142,26 @@ class SentryEventDetailsObservation(Observation): def render(self) -> str: """Render the event details view.""" event = self.event_data - + header = f"[SENTRY EVENT]: {event.get('eventID', 'Unknown')}" header += f"\nOrganization: {self.organization_slug}" header += f"\nProject: {self.project_slug}" header += f"\nTitle: {event.get('title', 'Untitled')}" - + details = f"\nTimestamp: {event.get('dateCreated', 'Unknown')}" - + # Add user info if available user = event.get('user') if user: details += f"\nUser: {user.get('username') or user.get('email') or user.get('ip_address') or 'Anonymous'}" - + # Add tags tags = event.get('tags', []) if tags: details += "\nTags:" for tag in tags: details += f"\n - {tag.get('key', '')}: {tag.get('value', '')}" - + # Add exception info if available exception_entries = [entry for entry in event.get('entries', []) if entry.get('type') == 'exception'] if exception_entries: @@ -169,7 +169,7 @@ def render(self) -> str: for entry in exception_entries: for exception in entry.get('data', {}).get('values', []): details += f"\n\n### {exception.get('type', 'Unknown')}: {exception.get('value', 'Unknown')}" - + # Add stack trace frames = exception.get('stacktrace', {}).get('frames', []) if frames: @@ -179,12 +179,12 @@ def render(self) -> str: function = frame.get('function', 'Unknown') lineno = frame.get('lineno', '?') details += f"\n - {filename}:{lineno} in {function}" - + # Add context if available context_line = frame.get('context_line') if context_line: details += f"\n {context_line.strip()}" - + return f"{header}{details}" @@ -212,7 +212,7 @@ def view_sentry_issues( # Get auth token and installation UUID auth_token = get_sentry_auth_token() installation_uuid = get_installation_uuid(organization_slug) - + if not auth_token: return SentryIssuesObservation( status="error", @@ -224,14 +224,14 @@ def view_sentry_issues( issues=[], has_more=False, ) - + if not installation_uuid: available_orgs = get_available_organizations() if not available_orgs: org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." else: org_message = f"Available organizations: {', '.join(available_orgs.keys())}" - + return SentryIssuesObservation( status="error", error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", @@ -242,10 +242,10 @@ def view_sentry_issues( issues=[], has_more=False, ) - + # Initialize Sentry client client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) - + # Get issues response = client.get_issues( organization_slug=organization_slug, @@ -255,21 +255,21 @@ def view_sentry_issues( limit=limit, cursor=cursor, ) - + # Extract pagination info has_more = False next_cursor = None - + if isinstance(response, dict): issues = response.get('data', []) - + # Check for pagination info pagination = response.get('pagination', {}) has_more = pagination.get('hasMore', False) next_cursor = pagination.get('nextCursor') else: issues = response - + return SentryIssuesObservation( status="success", organization_slug=organization_slug, @@ -280,7 +280,7 @@ def view_sentry_issues( has_more=has_more, next_cursor=next_cursor, ) - + except Exception as e: return SentryIssuesObservation( status="error", @@ -314,7 +314,7 @@ def view_sentry_issue_details( # Get auth token and installation UUID auth_token = get_sentry_auth_token() installation_uuid = get_installation_uuid(organization_slug) - + if not auth_token: return SentryIssueDetailsObservation( status="error", @@ -325,14 +325,14 @@ def view_sentry_issue_details( events=[], has_more_events=False, ) - + if not installation_uuid: available_orgs = get_available_organizations() if not available_orgs: org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." else: org_message = f"Available organizations: {', '.join(available_orgs.keys())}" - + return SentryIssueDetailsObservation( status="error", error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", @@ -342,13 +342,13 @@ def view_sentry_issue_details( events=[], has_more_events=False, ) - + # Initialize Sentry client client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) - + # Get issue details issue = client.get_issue_details(issue_id=issue_id, organization_slug=organization_slug) - + # Get events for the issue events_response = client.get_issue_events( issue_id=issue_id, @@ -356,21 +356,21 @@ def view_sentry_issue_details( limit=limit, cursor=cursor, ) - + # Extract pagination info has_more = False next_cursor = None - + if isinstance(events_response, dict): events = events_response.get('data', []) - + # Check for pagination info pagination = events_response.get('pagination', {}) has_more = pagination.get('hasMore', False) next_cursor = pagination.get('nextCursor') else: events = events_response - + return SentryIssueDetailsObservation( status="success", organization_slug=organization_slug, @@ -380,7 +380,7 @@ def view_sentry_issue_details( has_more_events=has_more, next_cursor=next_cursor, ) - + except Exception as e: return SentryIssueDetailsObservation( status="error", @@ -411,7 +411,7 @@ def view_sentry_event_details( # Get auth token and installation UUID auth_token = get_sentry_auth_token() installation_uuid = get_installation_uuid(organization_slug) - + if not auth_token: return SentryEventDetailsObservation( status="error", @@ -421,14 +421,14 @@ def view_sentry_event_details( event_id=event_id, event_data={}, ) - + if not installation_uuid: available_orgs = get_available_organizations() if not available_orgs: org_message = "No Sentry organizations configured. Please set the SENTRY_CODEGEN_INSTALLATION_UUID or SENTRY_RAMP_INSTALLATION_UUID environment variables." else: org_message = f"Available organizations: {', '.join(available_orgs.keys())}" - + return SentryEventDetailsObservation( status="error", error=f"Sentry installation UUID not found for organization '{organization_slug}'. {org_message}", @@ -437,17 +437,17 @@ def view_sentry_event_details( event_id=event_id, event_data={}, ) - + # Initialize Sentry client client = SentryClient(auth_token=auth_token, installation_uuid=installation_uuid) - + # Get event details event = client.get_event_details( event_id=event_id, organization_slug=organization_slug, project_slug=project_slug, ) - + return SentryEventDetailsObservation( status="success", organization_slug=organization_slug, @@ -455,7 +455,7 @@ def view_sentry_event_details( event_id=event_id, event_data=event.dict() if hasattr(event, 'dict') else event, ) - + except Exception as e: return SentryEventDetailsObservation( status="error", @@ -464,4 +464,4 @@ def view_sentry_event_details( project_slug=project_slug, event_id=event_id, event_data={}, - ) \ No newline at end of file + )