Metadata-Version: 2.4
Name: muffin-sentry
Version: 2.1.0
Summary: Sentry Integration for Muffin framework
Keywords: sentry,asyncio,trio,asgi,muffin
Author: Kirill Klenov
Author-email: Kirill Klenov <horneds@gmail.com>
License-Expression: MIT
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Framework :: AsyncIO
Classifier: Framework :: Trio
Requires-Dist: muffin
Requires-Dist: sentry-sdk>=2,<3
Requires-Python: >=3.11, <4
Project-URL: Homepage, https://github.com/klen/muffin-sentry
Project-URL: Repository, https://github.com/klen/muffin-sentry
Description-Content-Type: text/markdown

# Muffin-Sentry

**Muffin-Sentry** — [Sentry](https://sentry.io/) integration for the [Muffin](https://github.com/klen/muffin) ASGI framework.

![Tests Status](https://github.com/klen/muffin-sentry/workflows/tests/badge.svg)
[![PyPI Version](https://img.shields.io/pypi/v/muffin-sentry)](https://pypi.org/project/muffin-sentry/)
[![Python Versions](https://img.shields.io/pypi/pyversions/muffin-sentry)](https://pypi.org/project/muffin-sentry/)
[![License](https://img.shields.io/github/license/klen/muffin-sentry)](https://opensource.org/licenses/MIT)

---

## Requirements

- Python 3.11 - 3.15
- Muffin
- sentry-sdk >= 2, < 3

## Installation

Install using pip:

```bash
pip install muffin-sentry
```

## Usage

```python
from muffin import Application
import muffin_sentry

app = Application("example", SENTRY_DSN="https://<public>@sentry.io/<project_id>")
sentry = muffin_sentry.Plugin(app)

# Add a custom processor (must be sync)
@sentry.processor
def enrich_event(event, hint, request):
    if user := getattr(request, "user", None):
        event["user"] = {"id": str(user.id)}
    return event

# Raise unhandled exception
@app.route("/fail")
async def fail(request):
    raise RuntimeError("Boom")

# Manually capture a message
@app.route("/capture")
async def capture(request):
    sentry.capture_message("Manual log")
    return "OK"

# Update scope manually
@app.route("/scope")
async def tag_scope(request):
    sentry.scope.set_tag("section", "test")
    sentry.capture_exception(Exception("With scope tag"))
    return "OK"
```

`Plugin(app)` calls `setup()` for you. Calling `muffin_sentry.Plugin()` and then `sentry.setup(app)` is
equivalent.

## Configuration

Options are read from the application config with the `SENTRY_` prefix (`SENTRY_DSN`,
`SENTRY_SDK_OPTIONS`, ...), or from environment variables with the `{CFG.NAME}_SENTRY_` prefix,
`MUFFIN_SENTRY_` by default. JSON is parsed for complex values:

```bash
export MUFFIN_SENTRY_DSN=https://<public>@sentry.io/<project_id>
export MUFFIN_SENTRY_SDK_OPTIONS='{"environment": "production", "traces_sample_rate": 0.05}'
```

Environment variables are applied by Muffin before the plugin is set up, so they win over the values
passed to `Application(...)`.

1. **Via Muffin application config (recommended)**:

```python
app = Application(
    "app",
    SENTRY_DSN="https://...",
    SENTRY_SDK_OPTIONS={"traces_sample_rate": 0.5},
)
```

2. **Or by calling `.setup()` manually**:

```python
sentry.setup(app, dsn="https://...", sdk_options={"traces_sample_rate": 0.5})
```

Available options:

| Name            | Default value                                              | Description                                                |
| --------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
| `dsn`           | `""`                                                       | Sentry DSN; when empty `sentry_sdk.init()` is never called |
| `sdk_options`   | `{"traces_sample_rate": 0.1, "profiles_sample_rate": 0.0}` | Keyword arguments for `sentry_sdk.init()`                  |
| `ignore_errors` | `()`                                                       | Exception classes the middleware reports no event for      |

`sdk_options` is replaced as a whole (no deep merge), so a custom dict has to repeat the defaults you
want to keep:

```python
app = Application(
    "app",
    SENTRY_DSN="https://...",
    SENTRY_SDK_OPTIONS={
        "environment": "production",
        "release": "myapp@1.4.2",
        "traces_sample_rate": 0.05,  # 10% default, 0 disables tracing
        "send_default_pii": True,  # send user IPs and headers to Sentry
    },
)
```

`ResponseError` (Muffin's 404) and `ResponseRedirect` (`muffin.redirect`) are handled by the middleware
itself: they are control flow, not failures, so no event is sent and the transaction gets the real
status code. `ignore_errors` is for your own exception classes and compares by exact type.

## API

### `plugin.scope`

Property returning the current Sentry `Scope` (`sentry_sdk.get_current_scope()`). Inside a request it is
the isolation scope created by the middleware, so tags, users and breadcrumbs set there are attached to
events captured during that request. Before `setup()` it returns the global scope; it never raises.

### `plugin.processor(fn)`

Decorator registering an event processor. `fn(event, hint, request)` is called synchronously from
`process_data()` after the request context (`url`, `query_string`, `method`, `headers`) has been added
to the event. `REMOTE_ADDR` is added only when `send_default_pii` is enabled. Return the modified
event, or `None` to keep the event unchanged. Processors are never awaited: an `async def` processor
returns a coroutine that is stored as the event, so keep them synchronous.

### `plugin.capture_exception(exc)` / `plugin.capture_message(msg)`

Proxies to `sentry_sdk.capture_exception()` / `capture_message()`, which do nothing when no Sentry
client is active (no `dsn` configured). The difference from calling the SDK directly is what is
attached: the request isolation scope, the plugin's tags (`framework`, `asgi.type`), the scope modified
by your processors and the active transaction all come along automatically.

### `plugin.middleware`

Runs around every request:

- opens an isolation scope, clears breadcrumbs, tags `framework=muffin` and `asgi.type`
  (`http`/`websocket`/...), and registers the event processor that fills `event["request"]`;
- starts a Sentry session for the request and ends it when the request is done, which is what release
  health reports;
- continues the incoming trace (`sentry-trace`/`baggage` headers) and starts a transaction named after
  the request path with `op="{asgi.type}.server"`, so distributed traces connect the frontend request
  with backend errors. The event is then renamed after the matched view
  (`module.function`), so routes with ids (`/users/42`) do not create one transaction name per id;
- sets the HTTP status on the transaction and captures unhandled exceptions, except the classes listed
  in `ignore_errors`.

Consequently a request that ends up with `ResponseError` (Muffin's 404, for example) produces a
transaction with status `not_found` but no error event. Disable tracing by setting
`traces_sample_rate` to `0` in `sdk_options`.

## Notes

- Event processors must be **synchronous** functions.
- Sentry sessions and transactions are handled automatically inside the plugin middleware.
- `sentry_sdk.init()` is called only when `dsn` is set; without it the plugin is a thin no-op.

## Bug Tracker

Found a bug or have a feature request?
Please open an issue at: https://github.com/klen/muffin-sentry/issues

## Contributing

Development happens at: https://github.com/klen/muffin-sentry

Pull requests and suggestions are welcome!

## License

Licensed under the [MIT license](https://opensource.org/licenses/MIT).

## Credits

- Created by [klen](https://github.com/klen) (Kirill Klenov)
