Skip to content

Reference

Public surface

Two names, both importable from pagebar:

from pagebar import PagebarMiddleware, pagebar_html

Plus pagebar.flask.Pagebar if you installed the [flask] extra.

What's on screen

Field Source
Version importlib.metadata.version(package) — or explicit version=
Time time.perf_counter() delta
SQL SQLAlchemy before_cursor_execute listener (if installed)
Request method + path
Memory resource.getrusage().ru_maxrss — RSS in MiB
Uptime time.monotonic() since worker import
Threads threading.active_count()
GC gc.get_count() — gen0/gen1/gen2 pending
Python sys.version_info
PID os.getpid()

No SQL text. No params. No env. No traces. That's the whole surface — the fixed field set is the security model, so adding a field is a security review, not a feature.

The SQL counter needs the [sqlalchemy] extra. Without SQLAlchemy installed, the listener registration is a silent no-op and the count stays at 0.

Knobs

PagebarMiddleware(
    app,
    package="",          # distribution name (PyPI / pyproject.toml)
    version="",          # escape hatch: bypass importlib.metadata lookup
    enabled=True,        # bool or callable(scope) -> bool
    unsafe=False,        # bool or callable(scope) -> bool — see below
    query_budget=0,      # >0 → log a WARNING when SQL count exceeds this
)

All keyword-only. pagebar.flask.Pagebar(app, ...) takes the same set, with enabled / unsafe callables receiving the Flask request instead of the ASGI scope.

PagebarMiddleware.bound(**kwargs)

Returns a thin subclass with the kwargs baked into __init__, for frameworks that instantiate middleware as cls(app). See Usage.

pagebar_html(*, nonce="")

Renders the footer for the current request. Returns HTML as str — mark it safe in your template. nonce is threaded into the inline <style> and <script> for CSP.

Unsafe mode

In dev, you usually want to see the SQL text. Flip unsafe=True and the panel gains a SQL summary — total, unique, and how many queries were redundant — plus a SQL details button:

The expanded panel in unsafe mode, with the SQL summary and details button

A red UNSAFE badge in the pill makes the mode obvious even when the panel is closed:

The pill in unsafe mode, with a red UNSAFE badge

The SQL modal

SQL details opens a full-screen modal. Up top, the grouped N+1 report: one row per unique statement with its repeat count and total time, repeats flagged in red.

The SQL modal's grouped N+1 report, with the full query list collapsed

Below it, All queries — collapsed by default — lists every statement in execution order with its bound parameters and per-statement timing. Statements are never truncated.

The SQL modal with the full query list expanded

The modal text is selectable, and Copy report puts a self-contained N+1 summary on the clipboard — request line, totals, and each unique statement with its call count — ready to paste into a bug report:

pagebar SQL report — GET /widgets
21 queries, 2 unique, 19 redundant (possible N+1)

20×  0.0ms  SELECT name FROM widget WHERE id = ?
 1×  0.1ms  SELECT id FROM widget

Turning it on

import os
PagebarMiddleware(app, package="my-app", unsafe=bool(os.getenv("DEBUG")))
# or framework-driven
PagebarMiddleware(app, package="my-app", unsafe=app.debug)

unsafe comes from deploy config only

It defaults to False and only takes its value from constructor wiring — no URL parameter, no header, no cookie. The host's deploy config is the authority, and there is deliberately no runtime backchannel to flip it.

How it works

Two halves, decoupled by ContextVars. The middleware can't hand data to the template helper directly — they sit at opposite ends of the request — so per-request state (start time, query log, method, path, version, unsafe flag) goes through module-level context variables. PagebarMiddleware.__call__ writes them at request start; pagebar_html() reads them when the template renders.

ASGI tasks get a fresh context per request. Flask's WSGI worker threads don't, so the extension resets the state in teardown_request — that reset is load-bearing: without it one request's data could surface on the next request served by the same thread.