Skip to main content

forge-ops-tracker

Python error reporting client for a ForgeOps instance. Requires Python 3.9+. It captures unhandled and explicitly reported exceptions, builds a backtrace, scrubs likely PII, and delivers events to ForgeOps over HTTP without blocking the request or process that raised them.

Installation

Not yet published to PyPI: install directly from this path (or a local checkout, once split into its own repo):

pip install -e path/to/forge_ops/sdks/python

For Django or Flask integration, install the matching extra:

pip install -e "path/to/forge_ops/sdks/python[django]"
pip install -e "path/to/forge_ops/sdks/python[flask]"

Configuration

Set a DSN (from a project's settings page in ForgeOps), either via the FORGE_OPS_DSN environment variable or explicitly:

import forge_ops_tracker

forge_ops_tracker.init(
    dsn="https://<api_key>@your-forgeops-host/api/v1/events",  # or leave unset to read FORGE_OPS_DSN
    release="...",
    environment="production",
)

Call init() once at startup: Django's settings.py, or right after creating a Flask app. Any Configuration attribute can be overridden by keyword.

Django

# settings.py
import forge_ops_tracker

forge_ops_tracker.init(dsn="https://<api_key>@your-forgeops-host/api/v1/events")

MIDDLEWARE = [
    ...,
    "forge_ops_tracker.integrations.django.ForgeOpsTrackerMiddleware",
    "forge_ops_tracker.integrations.django.ForgeOpsTrackerSessionTrackingMiddleware",
    "forge_ops_tracker.integrations.django.ForgeOpsTrackerPerformanceTrackingMiddleware",
]

Flask

from flask import Flask
import forge_ops_tracker
from forge_ops_tracker.integrations.flask import init_flask

forge_ops_tracker.init(dsn="https://<api_key>@your-forgeops-host/api/v1/events")

app = Flask(__name__)
init_flask(app)

What gets reported automatically, and what doesn't

An exception that crashes a request needs no further wiring at all. The Django middleware's process_exception hook and Flask's got_request_exception signal both fire for anything that propagates uncaught out of a view, then let the framework handle it exactly as if this client weren't installed.

An exception your own code catches and handles is different: neither integration ever sees it, since it never propagates far enough to reach either hook:

try:
    charge_card(order)
except CardError as e:
    logger.warning("card declined: %s", e)
    # ForgeOps never sees this: caught locally, never reaches the
    # middleware/signal at all.

There's no application-wide hook that reports an exception while still letting your own except block handle it: report it explicitly instead, right at the catch site:

except CardError as e:
    forge_ops_tracker.capture_exception(e, context={"order_id": order.id})
    logger.warning("card declined: %s", e)

Called with no arguments, capture_exception() picks up whichever exception is currently being handled (same as a bare raise inside an except: block), so it usually reads as just forge_ops_tracker.capture_exception() from inside the block that already caught it.

Outside a web request (scripts, management commands, workers)

init() also installs a sys.excepthook wrapper by default (Configuration.install_excepthook, True unless set otherwise), which reports anything that crashes the whole interpreter: a plain script, a Django management command, a worker's own top-level loop: with no wiring needed, the same "unhandled needs no wiring" case the Django/Flask integrations cover for web requests. It still calls whatever sys.excepthook was already installed afterward, so it never changes program behavior. This does not catch a web request's unhandled exception under a real WSGI server (Gunicorn/uWSGI catch that themselves per-request, long before it would ever reach the interpreter level): that's what the Django/Flask integrations are for.

Delivery happens on a background thread with a bounded queue and a short per-request HTTP timeout (Configuration.timeout, 2s default). Every failure mode: network errors, timeouts, a full queue, a malformed DSN: is caught and dropped rather than raised, so a broken or unreachable tracker can never take down the host app. The worker thread starts lazily, on first push, not at import time: Gunicorn (prefork) and uWSGI commonly fork worker processes after the application has already loaded, which would leave an eagerly-started thread dead in every forked child; starting fresh on first push means each forked worker gets its own live thread regardless of when it was forked relative to import.

in_app backtrace frames

Python runs interpreted directly from real .py files on disk, so file-path matching against Configuration.app_root is a straightforward prefix comparison against those on-disk paths. Defaults to the current working directory; set it explicitly if that doesn't match your app's actual layout (a WSGI server started from a different directory than your app's root, for instance). Standard-library and installed-package (site-packages/dist-packages) frames are never marked in_app, regardless of app_root.

PII scrubbing

By default, the message, backtrace, and any context/tags you attach are scanned for likely personal data (email addresses, formatted SSNs/credit cards, known API key/token formats, and anything under a suspiciously-named key like password, api_key, or ssn) and redacted before the payload ever leaves this process. ForgeOps itself scrubs again on arrival regardless, so this is a second, earlier layer, not the only one.

To disable it:

forge_ops_tracker.init(dsn="...", scrub_pii=False)

Source context

By default, each in-app backtrace frame (never a standard-library or installed-package frame) is captured along with the 5 lines of source on either side of the culprit line, read straight off disk at raise-time, so an issue's detail page can show the actual code that broke, not just a file:line:method reference. This never applies to a frame outside your configured app_root, and it fails silently (no context, not an exception) for any file that can't be read for whatever reason.

This is a real, deliberate exception to "off by default is safer": literal source code is being transmitted, not just a reference to it, and the real protection here is not this flag. Every project on ForgeOps has its own setting (on by default, off durably and immediately once an org owner turns it off, regardless of what any individual app's own capture_source_context is still set to) that governs whether the server will ever actually store what a client sends. Set this to False if you'd rather this client never even attempt the disk read in the first place:

forge_ops_tracker.init(dsn="...", capture_source_context=False)

Session tracking (release health)

By default, every request through the Django/Flask integrations is counted as a session: crash-free unless an unhandled exception (or a 5xx response, for Django, where the exception has already been converted to a response by the time this client ever sees the request) actually affects it, giving ForgeOps a crash-free rate per release to show alongside the errors themselves, not just the errors on their own. Counted in-process and flushed as a small periodic aggregate on a background thread (never one network call per request), the same delivery philosophy as everything else in this client: a broken or unreachable tracker never affects the host app either way.

forge_ops_tracker.init(
    dsn="...",
    track_sessions=False,       # opt out entirely
    session_flush_interval=30,  # seconds; default 60
)

Requires a ForgeOps plan that includes release health; on a plan that doesn't, the periodic flushes are simply rejected server-side and dropped, exactly like any other delivery failure.

Performance monitoring

By default, the Django/Flask integrations also time every request, so a dashboard widget on ForgeOps can show which parts of your app are actually slow, not just which ones raise. Bucketed by transaction ("GET /users/<int:id>", the matched URL pattern rather than the literal path, so a distinct user id doesn't explode into its own separate transaction) and flushed as a small periodic aggregate per transaction on the same kind of background thread session tracking above uses.

forge_ops_tracker.init(
    dsn="...",
    track_performance=False,        # opt out entirely
    performance_flush_interval=30,  # seconds; default 60
)

Requires a ForgeOps plan that includes performance monitoring; on a plan that doesn't, the periodic flushes are simply rejected server-side and dropped, exactly like any other delivery failure.

Running the tests

cd sdks/python
python3 -m venv .venv
./.venv/bin/pip install -e ".[test]"
./.venv/bin/python -m pytest
./.venv/bin/ruff check src tests

Release files for forge-ops-tracker 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for forge-ops-tracker 0.3.0
File Size Uploaded
forge_ops_tracker-0.3.0.tar.gz 29.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for forge-ops-tracker 0.3.0
File Interpreter ABI Platform
forge_ops_tracker-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 53.1 kB

Release files / forge_ops_tracker-0.3.0.tar.gz

Download URL forge_ops_tracker-0.3.0.tar.gz
Size 29.3 kB
Tags Source
SHA-256 checksum
How to use checksums
46b7184cc10fb85bd57b043de9c7e0bfc6f48adc35dc9adec410f63a0396adeb
BLAKE2b-256 checksum
How to use checksums
9c8751bf57fe586388e7c90db7ef092f3f444ce63d2e9229fcf50b0f8c24c25f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / forge_ops_tracker-0.3.0-py3-none-any.whl

Download URL forge_ops_tracker-0.3.0-py3-none-any.whl
Size 23.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4e9fe6c6b6b6648e395efbfbcdb42b69a391159f95f007031027e096e518fb51
BLAKE2b-256 checksum
How to use checksums
0c095e04a44a5efdd47695efae772b1c72859f878eb2472c71013f088ca5e1ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

0.12.0

2 release files

0.11.0

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.6.0

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page