From 7266d751d06cd63b1af833fa042750e2bf9265e4 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 25 Jun 2026 19:02:58 -0300 Subject: [PATCH] Set up Sentry --- mcp/pyproject.toml | 1 + mcp/src/flagsmith_mcp/config.py | 8 +++++++ mcp/src/flagsmith_mcp/server.py | 7 +++++- mcp/src/flagsmith_mcp/telemetry.py | 11 ++++++++++ mcp/tests/unit/test_telemetry.py | 34 ++++++++++++++++++++++++++++++ mcp/uv.lock | 8 ++++--- 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/mcp/pyproject.toml b/mcp/pyproject.toml index 1fb698f548dc..9e4a3834a5a1 100644 --- a/mcp/pyproject.toml +++ b/mcp/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "opentelemetry-instrumentation-httpx>=0.46b0,<1.0.0", # Trace upstream API calls "prometheus-client>=0.21.0,<1.0.0", # Export Prometheus metrics "pydantic-settings>=2.0.0,<3.0.0", # Environment-driven configuration + "sentry-sdk>=2.63.0,<3.0.0", # Capture errors and tracebacks ] [project.scripts] diff --git a/mcp/src/flagsmith_mcp/config.py b/mcp/src/flagsmith_mcp/config.py index 1b74010812a9..81ec72ada846 100644 --- a/mcp/src/flagsmith_mcp/config.py +++ b/mcp/src/flagsmith_mcp/config.py @@ -9,6 +9,10 @@ class Settings(BaseSettings): model_config = {"use_attribute_docstrings": True} + environment: Literal["local", "dev", "staging", "production"] = Field( + default="local", + ) + """Deployment environment.""" flagsmith_api_url: str = Field( default="https://api.flagsmith.com", ) @@ -47,6 +51,10 @@ class Settings(BaseSettings): ) """Public base URL of this MCP server, advertised in OAuth protected-resource metadata. Override for HTTP deployments behind a proxy/public hostname.""" + sentry_dsn: str | None = Field( + default=None, + ) + """Sentry DSN for error reporting. Error capture is disabled when unset.""" @model_validator(mode="after") def validate_stdio_token(self) -> "Settings": diff --git a/mcp/src/flagsmith_mcp/server.py b/mcp/src/flagsmith_mcp/server.py index 6ee857707664..7c9352c8bfcf 100644 --- a/mcp/src/flagsmith_mcp/server.py +++ b/mcp/src/flagsmith_mcp/server.py @@ -18,7 +18,11 @@ from flagsmith_mcp.metrics import PrometheusMiddleware from flagsmith_mcp.middleware import RootRouterMiddleware from flagsmith_mcp.oauth import FlagsmithResourceAuth -from flagsmith_mcp.telemetry import propagate_span_attributes, setup_telemetry +from flagsmith_mcp.telemetry import ( + propagate_span_attributes, + setup_sentry, + setup_telemetry, +) ROUTE_MAPS = [ RouteMap(tags={"mcp"}, mcp_type=MCPType.TOOL), @@ -92,6 +96,7 @@ async def health(request: Request) -> PlainTextResponse: def run() -> None: settings = config.Settings() setup_telemetry(settings) + setup_sentry(settings) server = create_server(settings) if settings.metrics_port is not None: start_http_server(settings.metrics_port) diff --git a/mcp/src/flagsmith_mcp/telemetry.py b/mcp/src/flagsmith_mcp/telemetry.py index 99039c9891ed..5a97d6dd7350 100644 --- a/mcp/src/flagsmith_mcp/telemetry.py +++ b/mcp/src/flagsmith_mcp/telemetry.py @@ -1,4 +1,5 @@ import httpx +import sentry_sdk from common.core.logging import setup_logging from common.core.otel import ( add_otel_trace_context, @@ -70,3 +71,13 @@ def setup_telemetry(settings: config.Settings) -> None: application_loggers=APPLICATION_LOGGERS, otel_processors=otel_processors, ) + + +def setup_sentry(settings: config.Settings) -> None: + """Initialise Sentry for error capture when a DSN is configured.""" + if not settings.sentry_dsn: + return + sentry_sdk.init( + dsn=settings.sentry_dsn, + environment=settings.environment, + ) diff --git a/mcp/tests/unit/test_telemetry.py b/mcp/tests/unit/test_telemetry.py index 25138c50fdc7..cb0cac8bd127 100644 --- a/mcp/tests/unit/test_telemetry.py +++ b/mcp/tests/unit/test_telemetry.py @@ -117,6 +117,40 @@ def test_client_info_span_processor__outside_request_context__service_identity_o ) +def test_setup_sentry__no_dsn__init_not_called(mocker: MockerFixture) -> None: + # Given + mocker.patch.dict(os.environ, {}, clear=True) + sentry_sdk_mock = mocker.patch.object(telemetry, "sentry_sdk", autospec=True) + empty_settings = config.Settings() + + # When + telemetry.setup_sentry(empty_settings) + + # Then + sentry_sdk_mock.init.assert_not_called() + + +def test_setup_sentry__dsn_set__initialises_error_capture( + mocker: MockerFixture, +) -> None: + # Given + mocker.patch.dict(os.environ, {}, clear=True) + sentry_sdk_mock = mocker.patch.object(telemetry, "sentry_sdk", autospec=True) + settings = config.Settings( + sentry_dsn="https://public@sentry.example/1", + environment="staging", + ) + + # When + telemetry.setup_sentry(settings) + + # Then + sentry_sdk_mock.init.assert_called_once_with( + dsn="https://public@sentry.example/1", + environment="staging", + ) + + async def test_propagate_span_attributes__no_recording_span__headers_untouched() -> ( None ): diff --git a/mcp/uv.lock b/mcp/uv.lock index 93aeb5af8716..8de86436d62e 100644 --- a/mcp/uv.lock +++ b/mcp/uv.lock @@ -658,6 +658,7 @@ dependencies = [ { name = "opentelemetry-instrumentation-httpx" }, { name = "prometheus-client" }, { name = "pydantic-settings" }, + { name = "sentry-sdk" }, ] [package.dev-dependencies] @@ -681,6 +682,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.46b0,<1.0.0" }, { name = "prometheus-client", specifier = ">=0.21.0,<1.0.0" }, { name = "pydantic-settings", specifier = ">=2.0.0,<3.0.0" }, + { name = "sentry-sdk", specifier = ">=2.63.0,<3.0.0" }, ] [package.metadata.requires-dev] @@ -1955,15 +1957,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.61.1" +version = "2.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" }, ] [[package]]