spacr.crashreport

Create an attachable diagnostic bundle with spacr-crashreport.

A bug report that says “it crashed” costs a round trip to ask for the log, a second one for the settings, a third for the versions, and by then the user has re-run the pipeline and the evidence is gone. This module produces one .zip the user can drag into a GitHub issue, and it is assembled out of the pieces spaCR already keeps rather than collected a second time:

  • spacr.doctor runs 17 checks over the installation — the running checkout, duplicate installs, the GPU, the Cellpose version, the project database, the settings — and every non-PASS row already carries the fix. Including it avoids asking support to re-derive what one command already knows.

  • spacr.runctx writes a per-run JSONL log keyed by run id, so “show me everything from the run that produced this” has an answer. The bundle carries that run’s log verbatim, not a filtered summary of it.

  • spacr.errors stamps each run’s status onto the artifact it wrote, so the report can say whether the last run finished — which is a different question from whether it crashed just now, and the more useful one when a user reports numbers rather than a traceback.

Design rules, all of them load-bearing:

Nothing here may raise. A crash reporter that crashes while reporting a crash destroys the only evidence there was. Every collector runs through _collect(), which turns a failure into a manifest entry naming what could not be gathered and why. This is the one place in the codebase where catching Exception and carrying on is the correct behaviour rather than a swallowed error, and the reason it is correct is that the failure is recorded in the output — the manifest is part of the bundle, so a missing section is visible to the report recipient instead of silently absent.

Bounded size. spacr.log rotates but a single run can still write hundreds of megabytes. The bundle takes the tail of it, capped by MAX_LOG_BYTES, and the manifest records how much was dropped. An attachment nobody can upload is not evidence.

Nothing secret. Environment variables are included because SPACR_*, CUDA_* and PATH explain a large fraction of “works here, not there” — but a value whose name looks like a credential is replaced with "<redacted>" and listed by name in the manifest, so you can see what was withheld and the report recipient can see that something was. Absolute paths are kept: they name the checkout, the plate and the database, and a report with them stripped cannot be acted on.

Deterministic layout. File names and ordering remain stable so repeated reports are quick to inspect.

Usage

python -m spacr.crashreport                     # last run, current folder
python -m spacr.crashreport --run-id 3f9c1a2b   # one particular run
python -m spacr.crashreport --db plate1/measurements/measurements.db \
    --settings measure.csv --app measure -o ~/spacr-bug.zip
from spacr.crashreport import report_exception, install_excepthook

install_excepthook()          # any unhandled crash writes a bundle
try:
    measure_crop(settings)
except Exception as exc:      # noqa: BLE001 - reporting, then re-raising
    print(report_exception(exc, settings=settings))
    raise

Classes

CrashReport

Everything gathered, before it is written anywhere.

Functions

build_parser(→ argparse.ArgumentParser)

Return the spacr-crashreport argument parser.

collect(→ CrashReport)

Gather everything worth attaching to a bug report.

find_last_run_id(→ str)

Return the id of the most recent run this machine logged.

install_excepthook(→ Callable)

Write a crash report on any unhandled exception, then behave as before.

main(→ int)

Command-line entry point. python -m spacr.crashreport.

report_exception(→ str)

Write a crash report about exception and return where it went.

write_crash_report(→ str)

Gather a report and write it as one zip.

Module Contents

class spacr.crashreport.CrashReport[source]

Everything gathered, before it is written anywhere.

Kept as data rather than written straight to a zip so that a caller — a test, the Qt layer, a support script — can inspect or re-render it without a temporary file, and so that write_crash_report() has nothing in it but serialisation.

Parameters:
  • created_utc – when the report was made, ISO-8601.

  • run_id – the run it is about, empty when none could be identified.

  • sections – file name to text, exactly as it will appear in the zip.

  • manifest – what was collected, what was not, and why. Every entry that failed carries the exception text; every entry that was truncated carries the byte counts.

summary() → str[source]

One short block naming the run, the versions and what is missing.

This is also summary.txt, the first file in the bundle, because a report recipient opening a zip should not have to guess which file to read first.

Returns:

the summary text.

property omitted: List[str][source]

Names of the sections that had nothing to collect.

No settings file was named, the project has no database, the run wrote no warnings. Each is a fact about the invocation rather than a failure, and each is still listed, because the report recipient must be able to tell “there were no warnings” from “the warnings were not gathered”.

Returns:

section names, in collection order.

property problems: List[str][source]

Names of the sections that failed while being gathered.

Distinct from omitted, and the distinction matters: a section that had nothing to collect is an ordinary answer, and listing the two together under one alarming heading would train a reader to skip both.

Returns:

section names, in collection order. Empty on a clean report.

spacr.crashreport.build_parser() → argparse.ArgumentParser[source]

Return the spacr-crashreport argument parser.

Returns:

the parser, built separately so a test can exercise the argument surface without running a collection.

spacr.crashreport.collect(run_id: str | None = None, *, db: os.PathLike | None = None, settings: os.PathLike | None = None, settings_values: Mapping[str, Any] | None = None, checkout: os.PathLike | None = None, app: str = '', exception: BaseException | None = None, note: str = '', probe_gpu: bool = False) → CrashReport[source]

Gather everything worth attaching to a bug report.

Composed from spacr.doctor, spacr.runctx, spacr.errors and spacr.logging_util rather than collected again, so the report cannot disagree with what those tools say when run directly.

Parameters:
  • run_id – the run to include. Defaults to find_last_run_id(), which prefers an active run_context over the newest log on disk.

  • db – project database, e.g. plate1/measurements/measurements.db. Adds the doctor’s database checks and the run-status stamps.

  • settings – settings csv/json that was run.

  • settings_values – the settings as a mapping, when the caller has them in memory. Takes precedence over settings.

  • checkout – directory the user believes they are editing; defaults to the current one, which is what makes “am I running this code” answerable.

  • app – app key for the settings file (mask, measure, …).

  • exception – the exception being reported, if any. Its traceback goes in verbatim.

  • note – free text from the user — what they were doing.

  • probe_gpu – let the doctor allocate on the GPU to prove it works. Off by default: a probe that fails would add a failure that is not the one being reported.

Returns:

the assembled CrashReport. Never raises; a section that could not be gathered is named in CrashReport.problems.

spacr.crashreport.find_last_run_id(problems: List[str] | None = None) → str[source]

Return the id of the most recent run this machine logged.

The run ids live one JSONL file per run under spacr.runctx.runs_log_dir(), so “the last run” is the newest file there. An active run wins over it: inside a run_context the crash being reported is this run, not the one before it.

Parameters:

problems – optional list that anything which went wrong on the way to the answer is appended to. The reason this exists rather than a bare return '': “no run was found” and “the run directory could not be read” are different answers, and a report that cannot tell them apart sends the report recipient looking for a run that was there all along. collect() passes one and puts it in the manifest.

Returns:

the run id, or '' when none could be identified. Never raises – a missing log directory is an ordinary answer here.

spacr.crashreport.install_excepthook(destination: os.PathLike | None = None, **kwargs: Any) → Callable[source]

Write a crash report on any unhandled exception, then behave as before.

Chains rather than replaces: the previous sys.excepthook still runs, so the traceback the user is used to seeing still appears, with one line after it saying where the bundle is.

Parameters:
Returns:

the hook that was installed, so a caller can restore sys.excepthook to what it was.

spacr.crashreport.main(argv: Sequence[str] | None = None) → int[source]

Command-line entry point. python -m spacr.crashreport.

Parameters:

argv – arguments, defaulting to sys.argv[1:].

Returns:

0 when the bundle was written, 1 when it could not be — and never anything else, because a crash reporter that exits non-zero for a section it could not gather would train people to ignore it.

spacr.crashreport.report_exception(exception: BaseException, destination: os.PathLike | None = None, **kwargs: Any) → str[source]

Write a crash report about exception and return where it went.

The call to make from an except block that is about to re-raise.

Parameters:
Returns:

the path written, or '' when even writing the file failed – which is reported on stderr rather than raised, because this is called from a failure path and must never replace the user’s exception with one of its own.

spacr.crashreport.write_crash_report(destination: os.PathLike | None = None, **kwargs: Any) → str[source]

Gather a report and write it as one zip.

Parameters:
  • destination – where to write. A directory gets spacr-crashreport-<run id or timestamp>.zip inside it; anything else is used as the file name. Defaults to <log dir>/spacr-crashreport-<...>.zip, which exists and is writable on every machine spaCR has ever logged on.

  • kwargs – passed straight to collect().

Returns:

the absolute path of the file written.

Raises:

OSError – only when the destination itself cannot be written, which is the one failure this function must not hide – a report the caller is told about but which is not on disk is worse than an error.

Example

from spacr.crashreport import write_crash_report
path = write_crash_report(db='plate1/measurements/measurements.db')
print(f'attach {path} to the issue')

Nested helpers

_doctor_sections.gather_json() → str

Return retained Doctor rows as JSON, or raise if collection failed.

spacr/crashreport.py:460

_doctor_sections.gather_text() → str

Run Doctor, retain its rows and summary, and return formatted text.

spacr/crashreport.py:442

_main_log_section.gather() → str | None

Return the capped main-log tail and record its path and size facts.

spacr/crashreport.py:575

_run_log_section.gather() → str | None

Return the capped run-log tail and record its path and size facts.

spacr/crashreport.py:475

_run_status_section.gather() → str | None

Return project run-status rows as JSON when a database provides them.

spacr/crashreport.py:555

_run_summary_section.gather() → str | None

Return WARNING-and-higher run records as readable text, if any.

spacr/crashreport.py:499

_settings_section.gather() → str | None

Return explicit or recorded settings as JSON, or None if absent.

spacr/crashreport.py:525

collect.environment() → str

Return redacted environment JSON and record every redacted name.

spacr/crashreport.py:676

install_excepthook.hook(kind, value, tb) → None

Report, then delegate to the hook that was installed before.

spacr/crashreport.py:793