Skip to content

abilian/flask-coverage

Repository files navigation

flask-coverage

Live code coverage for a running Flask application.

flask-coverage wraps coverage.py as a Flask extension and exposes a small debug blueprint at /debug/coverage. You can introspect what's been executed so far, take snapshots, view a per-file HTML report, and export the raw .coverage data — all from a running process, without restarting it.

It is designed for two scenarios:

  • Browser tests. Run your full Flask app under Playwright/Selenium/Cypress, drive it however you like, then read the live coverage report to see which paths your end-to-end tests actually reach.
  • Production / canary. Measure what code your live traffic exercises. Coverage measurement carries some overhead (typically <15% on Python 3.12+ with sys.monitoring), but for low-to-mid QPS services that's a reasonable trade for ground-truth dead-code detection.

Install

pip install flask-coverage

Requires Python ≥ 3.12, Flask ≥ 2.3, coverage ≥ 7.4.

Quickstart

from flask import Flask
from flask_coverage import FlaskCoverage

app = Flask(__name__)
FlaskCoverage(app)   # mounts /debug/coverage

Run your app and visit http://127.0.0.1:5000/debug/coverage/.

A runnable demo with a step-by-step walkthrough lives in examples/.

Endpoints

Mounted under /debug/coverage by default (override with FlaskCoverage(app, url_prefix="…")).

Method Path Purpose
GET / Dashboard: cache timestamp, snapshots panel, embedded text report
GET /report Cached text report (same format as coverage report)
GET /html/ Cached HTML report (coverage.py's native, per-file source views)
GET /files Cached JSON list: {file, statements, missing, covered, percent}
GET /export Cached merged .coverage data file
POST /refresh Regenerate the cache. Browser refresh does not.
POST /snapshot?label=… Take a labelled, timestamped copy into snapshot_dir
GET /snapshots JSON list of saved snapshots
GET /snapshots/<id> Download a saved snapshot's .coverage file
POST /reset Erase all collected data

POST endpoints content-negotiate: Accept: application/json returns JSON, anything else gets a 303 redirect to the dashboard with a flash message. Snapshots and refresh are designed to be operable from the dashboard without leaving it.

Configuration

Coverage settings are read from [tool.coverage.*] in pyproject.toml automatically (via coverage.py's native config support), or from .coveragerc / setup.cfg / tox.ini if present.

[tool.coverage.run]
source = ["myapp"]
parallel = true        # recommended for gunicorn/uwsgi (see Multi-worker)

[tool.coverage.report]
omit = ["*/migrations/*", "*/tests/*"]

Starting coverage early

For accurate measurement of module-level code, coverage must start before your application modules are imported. Three options, in order of preference:

  1. COVERAGE_PROCESS_START env var (best for gunicorn/uwsgi):

    export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml
    gunicorn myapp:app
  2. flask-coverage CLI shim:

    flask-coverage --app myapp run --debug
  3. Manual start_early() as the very first line in wsgi.py:

    from flask_coverage import start_early
    start_early()                    # before any of your app modules
    from myapp import create_app
    app = create_app()

If a Coverage instance is already running (any of the above, or pytest-cov in tests), FlaskCoverage(app) adopts it instead of creating a duplicate tracer.

Security

The /debug/coverage blueprint exposes filesystem paths for every measured source file — treat it as sensitive. Registration is fail-closed: it requires one of the following, or it raises RuntimeError:

  • app.debug is True, or

  • FLASK_COVERAGE_PASSWORD env var is set (HTTP Basic auth — user admin, override with FLASK_COVERAGE_USERNAME), or

  • a custom auth= callback is passed to FlaskCoverage(...):

    FlaskCoverage(app, auth=lambda: current_user.is_authenticated and current_user.is_admin)

The basic-auth check uses hmac.compare_digest for constant-time comparison.

Operations

Disabling without redeploy

Set FLASK_COVERAGE_DISABLED to a truthy value (1, true, yes, on) before the process starts, and FlaskCoverage(app) becomes a no-op: no tracer, no blueprint, no auth check.

FLASK_COVERAGE_DISABLED=1 gunicorn myapp:app

Multi-worker (gunicorn / uwsgi)

Each worker traces independently. Set parallel = true under [tool.coverage.run] so each worker writes .coverage.<host>.<pid>.<rand>. Two things happen automatically:

  1. Auto-save thread. Each worker runs a daemon thread that calls cov.save() every autosave_interval seconds (default 30, configurable via FlaskCoverage(autosave_interval=…); set to 0 to disable). This bounds inter-worker staleness — when worker A serves /refresh, the on-disk data from workers B, C, D is at most 30s old.

  2. Non-destructive combine. Refreshing the cache merges the running worker's data with all sibling parallel files in a temp dir, runs coverage combine there, and uses the merged file. The original per-worker files on disk are never deleted, so workers keep accumulating data normally.

The dashboard surfaces both pieces of information: it shows the last-refresh timestamp and the autosave interval, so you can reason about freshness from the UI alone.

Caching and explicit refresh

/report, /files, /html/, and /export all serve from a file-based cache (.flask-coverage-cache/ next to the data file, configurable via cache_dir=…). Refreshing the browser never regenerates the cache — only POST /refresh does. This makes the cost of hitting the dashboard predictable, and prevents accidental thundering herds in production. Cache regeneration is file-locked across workers.

Performance

On Python 3.12+, coverage.py uses sys.monitoring (PEP 669), which is significantly faster than the legacy sys.settrace path — typically under 15% overhead. Acceptable for staging and canary; profile before turning on for high-QPS production traffic.

Development

git clone https://github.com/abilian/flask-coverage
cd flask-coverage
uv sync
uv run pytest          # 45 tests
make check             # lint + format + type check

Tests are organised by the test pyramid:

  • tests/a_unit/ — fast, isolated, mock-based
  • tests/b_integration/ — Flask test-client + mocked Coverage
  • tests/c_e2e/ — real coverage.Coverage running, including multi-worker simulation

CI runs ruff (lint + format), ty (type check), and pytest across Python 3.12 / 3.13 / 3.14.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors