Metadata-Version: 2.4
Name: nohmo
Version: 0.1.0
Summary: Server-side error tracking for Nohmo — product analytics and error monitoring in one tool.
Author: Nohmo
License-Expression: MIT
Project-URL: Homepage, https://www.nohmo.in
Project-URL: Documentation, https://www.nohmo.in/docs
Keywords: error tracking,monitoring,analytics,django,flask,fastapi
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Bug Tracking
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# Nohmo for Python (`nohmo`)

Server-side error tracking for [Nohmo](https://www.nohmo.in) — product analytics and error monitoring in one tool.

Errors raised on your backend land in the same project as your frontend errors, so a 500 in your API sits next to the JS error it caused in the browser. They flow into the same Errors page, the same revenue-impact ranking, and the same 6pm digest.

**No runtime dependencies.** Standard library only — an error reporter should never be the thing that breaks the app it watches.

## Install

```bash
pip install nohmo
```

```python
import nohmo_sdk
```

You install `nohmo` but import `nohmo_sdk`. The two names are deliberately different, the
same way `pip install pillow` gives you `import PIL`.

The import name is the one that has to stay out of the way. A package named `nohmo` in
site-packages shadows any `nohmo` package in your own project — and a Django project called
`nohmo` is not hypothetical, it is what Nohmo itself runs on. Keeping the import distinct
means the top-level name stays yours.

## Django

```python
# settings.py
NOHMO_PROJECT_ID = "proj_xxx"
NOHMO_API_KEY    = "pk_xxx"
NOHMO_RELEASE    = "2.4.1"          # optional, correlates errors with deploys
NOHMO_ENVIRONMENT = "production"    # optional

MIDDLEWARE = [
    "nohmo_sdk.integrations.django.NohmoMiddleware",   # near the top
    ...
]
```

Put it high in `MIDDLEWARE`. It cannot see exceptions raised by middleware **above** it, and it uses Django's `process_exception` hook — so your error pages, DRF handlers and everything else behave exactly as before.

## Flask

```python
import nohmo_sdk as nohmo
from nohmo_sdk.integrations.flask import NohmoFlask

app = Flask(__name__)
nohmo.init(project_id="proj_xxx", api_key="pk_xxx")
NohmoFlask(app)
```

## FastAPI

```python
import nohmo_sdk as nohmo
from nohmo_sdk.integrations.fastapi import NohmoMiddleware

nohmo.init(project_id="proj_xxx", api_key="pk_xxx")
app = FastAPI()
app.add_middleware(NohmoMiddleware)
```

Starlette applies middleware in reverse registration order, so add this **last** to have it outermost.

## Anything else (WSGI)

```python
from nohmo_sdk.integrations.wsgi import NohmoWSGI
application = NohmoWSGI(application)
```

## Manual capture

```python
import nohmo_sdk as nohmo

nohmo.init(project_id="proj_xxx", api_key="pk_xxx")

try:
    charge(order)
except PaymentError as exc:
    nohmo.capture_exception(exc, extra={"order_id": order.id})

nohmo.capture_message("Reconciliation found a mismatch", level="warning")
```

## Short-lived processes

`atexit` handles normal shutdown, but a management command, cron job or Lambda can exit before the queue drains. Flush explicitly:

```python
nohmo.flush(timeout=3)
```

## Options

| Option | Default | What it does |
|---|---|---|
| `environment` | `"production"` | Tags every event |
| `release` | `""` | Ties errors to a deploy for the causal narrative |
| `server_name` | hostname | Groups errors per server instance |
| `sample_rate` | `1.0` | Fraction of events sent |
| `dedup_window` | `5.0` | Seconds before an identical error is sent again |
| `queue_size` | `1000` | Bounded — drops rather than growing without limit |
| `batch_size` | `50` | Events per request |
| `flush_interval` | `5.0` | Seconds before a partial batch ships |
| `send_default_pii` | `False` | Include headers, query strings and user email |
| `debug` | `False` | Verbose logging |

## What it will and won't do

**Won't block your requests.** Capturing is a queue append; all I/O is on a background thread. A slow or dead Nohmo is invisible to your latency.

**Won't crash your app.** Every public entry point swallows its own exceptions. Calling anything before `init()` is a no-op, not an error — a missing env var cannot break a deploy.

**Won't grow without bound.** The queue is capped. A crash loop that outruns the network drops events and logs the count rather than consuming memory until the OOM killer arrives.

**Won't leak credentials.** `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, CSRF tokens, passwords and anything containing `secret`/`token`/`password`/`private_key` are redacted — matched after normalising case and hyphens. Headers, query strings and user emails are withheld entirely unless you set `send_default_pii=True`.

**Survives a fork.** gunicorn and uWSGI preload your app and then fork. Threads do not survive `fork()`, so an SDK that starts its worker in the master silently sends nothing from every child. This one tracks the pid it started under and restarts in the child.

## Grouping

Errors group by exception type plus the deepest frame in *your* code — framework and site-packages frames are skipped, and only the file basename and line are used, so the same bug groups across containers, virtualenvs and dev machines.

## Development

```bash
python -m pytest tests/ -v
```
