Skip to content

feat: generate privacy-safe local diagnostics bundle (#347) - #361

Closed
Shiva210Jyoti wants to merge 5 commits into
Abhash-Chakraborty:mainfrom
Shiva210Jyoti:main
Closed

feat: generate privacy-safe local diagnostics bundle (#347)#361
Shiva210Jyoti wants to merge 5 commits into
Abhash-Chakraborty:mainfrom
Shiva210Jyoti:main

Conversation

@Shiva210Jyoti

@Shiva210Jyoti Shiva210Jyoti commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Resolves #347

Summary

Adds a privacy-safe local diagnostics bundle for production support without telemetry or external uploads.

Files Created

  • backend/src/find_api/diagnostics/bundle.py — collector (version, runtime, Alembic state, PG/Redis/MinIO health, queue stats, ML config, last 20 sanitized errors)
  • backend/src/find_api/diagnostics/redact.py — allowlist-first recursive redaction + string scrubbing
  • backend/src/find_api/diagnostics/__init__.py
  • backend/src/find_api/routers/diagnostics.pyGET /api/admin/diagnostics/bundle admin-only endpoint
  • backend/tests/test_diagnostics_redact.py — 24 tests, all passing
  • docs/diagnostics-bundle.md — full include/exclude documentation
  • Wired router in main.py, linked from docs/index.md

Acceptance Criteria

Requirement Status
Collect app version, runtime, migration state, service health, queue depth, failed jobs, model state, sanitized errors
Redact passwords, tokens, storage keys, paths, filenames, captions, OCR, embeddings, faces, user IDs
Local only, explicit user action required
JSON archive suitable for GitHub issue attachment
Redaction tests with seeded secrets and private metadata ✅ 24 passing
Document what bundle includes and excludes

Notes

  • Deny-by-default redaction — allowlist first, never blocklist-only
  • X-Find-Diagnostics: local-only header + download disposition
  • No ML inference — model section reports config names and ML_MODE only
  • All 24 redaction tests pass, 0 failures

Summary by CodeRabbit

  • New Features

    • Added an admin-only endpoint to download a locally generated diagnostics bundle.
    • Bundle includes application health, service status, queue information, model details, migration state, and recent errors.
    • Sensitive data is automatically redacted, including credentials, paths, filenames, private media metadata, and tokens.
    • Downloads are clearly marked as local-only.
  • Documentation

    • Added usage instructions, privacy guarantees, and troubleshooting guidance for diagnostics bundles.
    • Linked the new guide from the documentation index.
  • Tests

    • Added comprehensive coverage for redaction and privacy protections.

Copilot AI review requested due to automatic review settings July 14, 2026 05:41
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

PR Context Summary

  • Linked issue(s): feat: generate a privacy-safe local diagnostics bundle #347
  • Referenced but not closing: none
  • PR author trusted by GitHub: no
  • Dependabot PR: no
  • PR assignee synced from linked issue: yes
  • Macroscope review status: Already triggered once for this PR. Use the workflow dispatch to manually rerun.

Suggested issue links

  • No strong issue match found yet.

Use Fixes #123 or Closes #123 in the PR body when one of the suggestions is the intended issue.
Manual rerun: Actions > PR Context Triage > Run workflow > set pr_number and force_review=true.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies #347 by adding local diagnostics collection, redaction, admin-only export, tests, and documentation.
Out of Scope Changes check ✅ Passed The changes shown are all directly related to the diagnostics bundle, redaction, routing, tests, and docs.
Title check ✅ Passed The title clearly and concisely describes the main change: generating a privacy-safe local diagnostics bundle.
Description check ✅ Passed The description covers the change, issue, acceptance criteria, testing, privacy behavior, and documentation, but omits several template checklist sections.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitguardian

gitguardian Bot commented Jul 14, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a local-only, privacy-redacted diagnostics bundle to support production troubleshooting without telemetry or external uploads. This fits Find’s local-first support posture by providing an explicit, admin-requested JSON export with deny-by-default redaction and documentation.

Changes:

  • Introduces a diagnostics collector (collect_diagnostics_bundle) that gathers runtime/app/service/queue/model status plus recent sanitized errors, then redacts the payload before returning it.
  • Adds an admin-only (shared mode) FastAPI export route GET /api/admin/diagnostics/bundle with attachment/download headers.
  • Adds an allowlist-first redaction + string scrubbing module with a focused test suite and user-facing documentation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
docs/index.md Adds docs navigation entry for the diagnostics bundle guide.
docs/diagnostics-bundle.md Documents generation steps and what is included/excluded in the bundle.
backend/tests/test_diagnostics_redact.py Adds redaction/scrubbing tests and a collector “shape + no leakage” test.
backend/src/find_api/routers/diagnostics.py Adds the admin export endpoint and installs the in-process error buffer.
backend/src/find_api/main.py Wires the new diagnostics router into the FastAPI app.
backend/src/find_api/diagnostics/redact.py Implements allowlist-first recursive redaction and string scrubbing.
backend/src/find_api/diagnostics/bundle.py Implements bundle collection (health checks, migrations, queue stats, model state, recent errors) and redaction.
backend/src/find_api/diagnostics/init.py Adds a small package entry point with lazy attribute loading.

Comment on lines +165 to +186
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from sqlalchemy import create_engine

backend_root = Path(__file__).resolve().parents[3]
ini_path = backend_root / "alembic.ini"
if not ini_path.is_file():
return {
"status": "unavailable",
"current": None,
"heads": [],
"detail": "alembic.ini not found",
}

cfg = Config(str(ini_path))
cfg.set_main_option("script_location", str(backend_root / "alembic"))
script = ScriptDirectory.from_config(cfg)
heads = list(script.get_heads())

engine = create_engine(settings.DATABASE_URL)
with engine.connect() as conn:
Comment on lines +64 to +76
_error_buffer = _ErrorLogBuffer()
_buffer_installed = False


def ensure_error_log_buffer() -> None:
"""Attach the in-process error ring buffer to the root logger once."""
global _buffer_installed
if _buffer_installed:
return
root = logging.getLogger()
if not any(isinstance(h, _ErrorLogBuffer) for h in root.handlers):
root.addHandler(_error_buffer)
_buffer_installed = True
Comment on lines +40 to +44
return JSONResponse(
content=bundle,
headers={
"Content-Disposition": 'attachment; filename="find-diagnostics-bundle.json"',
"X-Find-Diagnostics": "local-only",
Comment thread docs/diagnostics-bundle.md Outdated
Comment thread backend/src/find_api/diagnostics/bundle.py Outdated
Comment thread backend/src/find_api/diagnostics/redact.py
Comment thread backend/src/find_api/diagnostics/redact.py
Comment thread backend/src/find_api/diagnostics/bundle.py Outdated
Comment thread backend/src/find_api/diagnostics/bundle.py Outdated
Comment thread backend/src/find_api/diagnostics/redact.py
Comment thread backend/src/find_api/diagnostics/bundle.py
@macroscopeapp

macroscopeapp Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Unable to check for correctness in 5397774. New diagnostics feature introducing an admin endpoint, collection logic, and redaction system. Multiple unresolved review comments identify bugs (connection pool leak, thread safety issue, potential info exposure). Author does not own any changed files - designated code owner should review.

You can customize Macroscope's approvability policy. Learn more.

Comment thread backend/src/find_api/routers/diagnostics.py Fixed
@Abhash-Chakraborty
Abhash-Chakraborty self-requested a review July 14, 2026 05:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
backend/src/find_api/diagnostics/redact.py (1)

86-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sensitive-term lists are duplicated across four patterns; consider a single source of truth.

_SENSITIVE_KEY_RE, _SENSITIVE_KEY_SUBSTRING_RE, _SECRET_ASSIGN_RE, and _PRIVATE_FIELD_ASSIGN_RE each hardcode overlapping (but not identical) term lists. Adding a new sensitive field later requires remembering to update multiple regexes correctly — easy to miss one and silently under-redact.

♻️ Sketch: consolidate shared terms
+_CREDENTIAL_TERMS = (
+    "password", "passwd", "secret", "token", "api[_-]?key", "access[_-]?key",
+    "secret[_-]?key", "authorization", "auth", "credential", "credentials",
+)
+_PRIVATE_MEDIA_TERMS = (
+    "caption", "ocr(?:_text)?", "embedding", "vector", "face(?:s)?",
+    "person", "people", "filename", "filepath", "file_path", "minio_key",
+    "user(?:_?id)?", "uploader", "email", "username",
+)
+
 _SENSITIVE_KEY_RE = re.compile(
-    r"(?i)^(password|passwd|secret|token|api[_-]?key|access[_-]?key|"
-    r"secret[_-]?key|authorization|auth|credential|credentials|"
-    r"session|cookie|bearer|private[_-]?key|minio_key|thumbnail_key|"
-    r"filename|filepath|file_path|path|object_name|caption|ocr|"
-    r"ocr_text|embedding|vector|face|faces|person|people|"
-    r"user(_?id)?|uploader|email|username|display_name|"
-    r"database_url|redis_url|remote_ml_url|remote_ml_api_key|"
-    r"metadata_json|exif_json|file_hash)$"
+    rf"(?i)^(?:{'|'.join(_CREDENTIAL_TERMS + _PRIVATE_MEDIA_TERMS)}|"
+    r"session|cookie|bearer|private[_-]?key|thumbnail_key|path|"
+    r"object_name|display_name|database_url|redis_url|remote_ml_url|"
+    r"remote_ml_api_key|metadata_json|exif_json|file_hash)$"
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/diagnostics/redact.py` around lines 86 - 147,
Consolidate the overlapping sensitive field names used by _SENSITIVE_KEY_RE,
_SENSITIVE_KEY_SUBSTRING_RE, _SECRET_ASSIGN_RE, and _PRIVATE_FIELD_ASSIGN_RE
into a shared source of truth. Build each pattern from that shared term set
while preserving each regex’s existing scope and matching semantics, including
terms unique to particular contexts. Update future additions to require changing
the centralized definition only.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/find_api/diagnostics/bundle.py`:
- Around line 104-120: Update _collect_migration_state to reuse the module’s
shared PostgreSQL engine instead of calling create_engine for each diagnostics
request, preserving its existing migration checks. Update _check_redis to ensure
the Redis client created by Redis.from_url is closed on both success and
failure, using cleanup that also runs when ping raises.
- Around line 36-65: Protect the shared deque in _ErrorLogBuffer with a
threading lock: acquire it while emit() appends the formatted record and while
snapshot() copies the records. Initialize the lock in __init__, and keep
formatting/error handling outside or compatible with the lock so logging
failures still use handleError(record).

In `@backend/src/find_api/routers/diagnostics.py`:
- Around line 29-46: Add a route-level test for export_diagnostics_bundle
covering the /api/admin/diagnostics/bundle endpoint: verify admin
authentication, the Content-Disposition and X-Find-Diagnostics response headers,
and that the response JSON matches the collected diagnostics bundle shape. Reuse
the existing test client and authentication fixtures and mock
collect_diagnostics_bundle where appropriate.

In `@backend/tests/test_diagnostics_redact.py`:
- Around line 16-22: Replace the recognizable JWT header literal used by
SECRETS[3] and test_strips_bearer_tokens with an equally long non-standard fake
bearer token, or add a narrowly scoped scanner suppression for this fixture.
Preserve the test coverage of _BEARER_RE and _TOKEN_RE while ensuring
GitGuardian no longer identifies the seeded value as a real JWT.

---

Nitpick comments:
In `@backend/src/find_api/diagnostics/redact.py`:
- Around line 86-147: Consolidate the overlapping sensitive field names used by
_SENSITIVE_KEY_RE, _SENSITIVE_KEY_SUBSTRING_RE, _SECRET_ASSIGN_RE, and
_PRIVATE_FIELD_ASSIGN_RE into a shared source of truth. Build each pattern from
that shared term set while preserving each regex’s existing scope and matching
semantics, including terms unique to particular contexts. Update future
additions to require changing the centralized definition only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b9630883-2545-405d-b8e5-8c1e6082b892

📥 Commits

Reviewing files that changed from the base of the PR and between de5f279 and 2aba1f3.

📒 Files selected for processing (8)
  • backend/src/find_api/diagnostics/__init__.py
  • backend/src/find_api/diagnostics/bundle.py
  • backend/src/find_api/diagnostics/redact.py
  • backend/src/find_api/main.py
  • backend/src/find_api/routers/diagnostics.py
  • backend/tests/test_diagnostics_redact.py
  • docs/diagnostics-bundle.md
  • docs/index.md

Comment on lines +36 to +65
class _ErrorLogBuffer(logging.Handler):
"""Keep the last N ERROR+ log records in memory for diagnostics."""

def __init__(self, capacity: int = ERROR_LOG_LIMIT) -> None:
super().__init__(level=logging.ERROR)
self._records: deque[dict[str, Any]] = deque(maxlen=capacity)

def emit(self, record: logging.LogRecord) -> None:
try:
message = self.format(record) if self.formatter else record.getMessage()
self._records.append(
{
"timestamp": datetime.fromtimestamp(
record.created, tz=timezone.utc
).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": scrub_string(message),
"source": "log",
}
)
except Exception: # noqa: BLE001 — never break the logging pipeline
self.handleError(record)

def snapshot(self) -> list[dict[str, Any]]:
return list(self._records)


_error_buffer = _ErrorLogBuffer()
_buffer_installed = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,260p' backend/src/find_api/diagnostics/bundle.py

Repository: Abhash-Chakraborty/Find

Length of output: 8554


🏁 Script executed:

sed -n '260,420p' backend/src/find_api/diagnostics/bundle.py

Repository: Abhash-Chakraborty/Find

Length of output: 4858


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('backend/src/find_api/diagnostics/bundle.py')
text = p.read_text()
for start, end in [(1, 260), (260, 420)]:
    print(f"\n--- lines {start}-{end} ---")
    for i, line in enumerate(text.splitlines(), 1):
        if start <= i <= end:
            print(f"{i:4}: {line}")
PY

Repository: Abhash-Chakraborty/Find

Length of output: 15695


🏁 Script executed:

python3 - <<'PY'
import threading, logging
from collections import deque
import time

d = deque(maxlen=10)
exc = []

def writer():
    for i in range(100000):
        d.append(i)

def reader():
    try:
        for _ in range(100000):
            list(d)
    except Exception as e:
        exc.append(repr(e))

ts = [threading.Thread(target=writer) for _ in range(2)] + [threading.Thread(target=reader) for _ in range(2)]
for t in ts:
    t.start()
for t in ts:
    t.join()
print(exc[:10])
PY

Repository: Abhash-Chakraborty/Find

Length of output: 164


🏁 Script executed:

rg -n "snapshot\(\)|_collect_recent_errors|_error_buffer|deque mutated" backend/src/find_api/diagnostics/bundle.py

Repository: Abhash-Chakraborty/Find

Length of output: 389


Guard _error_buffer with a lock
emit() and snapshot() share a module-level deque across logging threads. Converting it to list() while another thread appends can raise RuntimeError: deque mutated during iteration, and _collect_recent_errors() doesn’t catch that path, so the diagnostics bundle can fail intermittently. Lock both append and snapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/diagnostics/bundle.py` around lines 36 - 65, Protect the
shared deque in _ErrorLogBuffer with a threading lock: acquire it while emit()
appends the formatted record and while snapshot() copies the records. Initialize
the lock in __init__, and keep formatting/error handling outside or compatible
with the lock so logging failures still use handleError(record).

Comment thread backend/src/find_api/diagnostics/bundle.py
Comment on lines +29 to +46
@router.get("/admin/diagnostics/bundle")
def export_diagnostics_bundle(
_admin: Optional[User] = Depends(get_admin_user),
):
"""Return a privacy-safe local diagnostics bundle as JSON.

Admin-only in shared mode (open in local mode), matching other
instance-wide admin endpoints. Requires an explicit HTTP request —
no background telemetry or outbound upload is performed.
"""
bundle = collect_diagnostics_bundle()
return JSONResponse(
content=bundle,
headers={
"Content-Disposition": 'attachment; filename="find-diagnostics-bundle.json"',
"X-Find-Diagnostics": "local-only",
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "diagnostics/bundle|export_diagnostics_bundle" backend/tests

Repository: Abhash-Chakraborty/Find

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## route references\n'
rg -n "diagnostics/bundle|export_diagnostics_bundle|collect_diagnostics_bundle|X-Find-Diagnostics|find-diagnostics-bundle" backend

printf '\n## diagnostics-related tests\n'
rg -n "diagnostic|redact|bundle|admin/diagnostics" backend/tests

printf '\n## test file inventory\n'
git ls-files backend/tests

Repository: Abhash-Chakraborty/Find

Length of output: 7220


Add a route-level test for /api/admin/diagnostics/bundle. backend/tests/test_diagnostics_redact.py exercises collect_diagnostics_bundle(), but the endpoint still needs coverage for admin auth, response headers, and the returned JSON shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/find_api/routers/diagnostics.py` around lines 29 - 46, Add a
route-level test for export_diagnostics_bundle covering the
/api/admin/diagnostics/bundle endpoint: verify admin authentication, the
Content-Disposition and X-Find-Diagnostics response headers, and that the
response JSON matches the collected diagnostics bundle shape. Reuse the existing
test client and authentication fixtures and mock collect_diagnostics_bundle
where appropriate.

Source: Path instructions

Comment thread backend/tests/test_diagnostics_redact.py
Comment thread backend/src/find_api/diagnostics/bundle.py
Shiva210Jyoti added a commit to Shiva210Jyoti/Find that referenced this pull request Jul 14, 2026
…raborty#361

- Fix _URL_CREDS_RE to handle password-only URLs (redis://:pass@host)
- Rename sensitive dict keys to redacted_key instead of preserving name
- Scrub free-standing quoted strings via _QUOTED_CONTENT_RE
- Add lock to snapshot() for thread safety
- Extend filename scrub to any word.ext pattern
- Local storage check uses is_dir() + os.access W_OK
- Reuse global engine in _collect_migration_state
- Clarify docs: hostnames may appear, only URL credentials stripped
- Router returns scrubbed 500 JSON, no stack traces exposed
- 508 tests passing
…rty#347)

- Add diagnostics/bundle.py to collect version, runtime, Alembic state, service health, queue stats, ML config, sanitized errors
- Add diagnostics/redact.py with allowlist-first recursive redaction
- Add GET /api/admin/diagnostics/bundle admin-only endpoint
- Add 24 passing redaction tests
- Add docs/diagnostics-bundle.md
- Wire router in main.py, link from docs/index.md
- Local JSON only, no uploads or telemetry

fix: address Copilot review and clear GitGuardian secrets in tests

- Use existing global engine in _collect_migration_state to avoid pool leaks
- Dedupe error log buffer handlers by name for uvicorn reload safety
- Add API tests covering headers, schema_version, auth (28 tests passing)
- Replace realistic-looking test fixtures with obvious placeholders

fix: address all review comments on diagnostics bundle PR Abhash-Chakraborty#361

- Fix _URL_CREDS_RE to handle password-only URLs (redis://:pass@host)
- Rename sensitive dict keys to redacted_key instead of preserving name
- Scrub free-standing quoted strings via _QUOTED_CONTENT_RE
- Add lock to snapshot() for thread safety
- Extend filename scrub to any word.ext pattern
- Local storage check uses is_dir() + os.access W_OK
- Reuse global engine in _collect_migration_state
- Clarify docs: hostnames may appear, only URL credentials stripped
- Router returns scrubbed 500 JSON, no stack traces exposed
- 508 tests passing

fix: replace scanner-facing test fixtures to clear GitGuardian

- Replace JWT-shaped bearer token with FAKE.TEST.TOKEN
- Replace sk-live API key with split sk-test-FAKE-KEY-FOR-TESTING-ONLY
- Replace passwords with EXAMPLE_PASSWORD_PLACEHOLDER
- 26 tests passing
Comment thread backend/src/find_api/routers/diagnostics.py Fixed
)

return JSONResponse(
content=bundle,
@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Please fix the CI.

Abhash-Chakraborty and others added 2 commits July 15, 2026 22:14
bundle.py: guard error log buffer with threading.Lock in emit()/snapshot() to prevent deque mutation under concurrent logging. redact.py: generalize filename scrubbing regex to catch non-allowlisted extensions (.txt, .csv) and dotfiles (.env, .gitignore) while avoiding false positives on version strings. routers/diagnostics.py: stop leaking exception class/message to clients on failure; log full exception server-side via logger.exception() and return a generic error message. test_diagnostics_redact.py and test_diagnostics_api.py: add coverage for new redaction cases and the diagnostics endpoint (headers, payload shape, secret leakage, generic 500, shared-mode auth).

Addresses review feedback from CodeRabbit, Macroscope, GitHub Copilot, and CodeQL on PR Abhash-Chakraborty#361.
Comment thread backend/src/find_api/diagnostics/bundle.py
Comment thread backend/src/find_api/diagnostics/redact.py
bundle.py: ensure_error_log_buffer now removes any stale same-named handler before attaching the current _error_buffer instance, fixing silent error-capture loss after uvicorn --reload. redact.py: fix _TOKEN_RE boundary so tokens ending in '-' are correctly redacted (previously leaked past the \b word boundary since '-' is a non-word character).

Adds test_strips_long_token_ending_in_hyphen regression test. Full suite: 561 passed, 5 skipped. Diagnostics: 34 passed.

Addresses Macroscope findings on PR Abhash-Chakraborty#361.
@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Really nice work — the allowlist-first redaction with per-string scrubbing on top is the right shape, and the 24-test redaction suite is what makes it trustworthy. Backend checks are green.

One blocker: this PR targets main, and the Branch policy check fails because of it. Contributor PRs must target canary; a merge into main is a release event. Please switch the base to canary (Edit button next to the title) and rebase if it complains.

Two smaller notes while you're in there:

  • _SENSITIVE_KEY_SUBSTRING_RE matches face and vector as substrings, so keys like interface or pgvector get scrubbed too. Harmless given deny-by-default, but worth a word in the docstring so the next person doesn't treat it as a bug.
  • docs/diagnostics-bundle.md should state that the endpoint is admin-only and never uploads anywhere, right at the top — that's the line people will quote when they paste a bundle into an issue.

Retarget to canary and I'll approve.

@Abhash-Chakraborty Abhash-Chakraborty added under-review Maintainer needs to verify backend FastAPI, database, storage, and API work gssoc26 Related to GirlScript Summer of Code 2026. type:feature Feature PR. GSSoC type bonus. privacy Data privacy, security boundaries, and user trust level:advanced GSSoC difficulty level: advanced. Base contributor points: 55. labels Jul 29, 2026
@github-actions

Copy link
Copy Markdown

@macroscope-app review

Please review this PR against its linked issue, local-first privacy rules, and the current Find repo instructions.
Linked issue(s): #347.
Trigger source: label-gated review (under-review).

@macroscopeapp

macroscopeapp Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Macroscope skipped reviewing this pull request. Per-PR cost limit exceeded (workspace setting).

Reviews on this PR have cost $4.74 so far. This review would add an estimated $0.50, bringing the total to $5.24 — above your per-PR limit of $5.00.

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude large or generated files from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Checking in — this has been sitting for a couple of weeks with Branch policy red, and I don't want it to rot because the work itself is good.

The only thing standing between it and review is the target branch. Contributor branches start from and target canary; main only ever moves through a maintainer promotion PR, so anything opened against it fails that check by design. I can't retarget it for you from here — main and canary have diverged far enough that switching the base would drag ~180 unrelated files into the diff.

What works:

git fetch upstream
git checkout -b feat/diagnostics-bundle-canary upstream/canary
git cherry-pick <your commits>
git push -u origin feat/diagnostics-bundle-canary

Then open a fresh PR against canary and link it here, or repoint this one if the diff comes out clean. I've marked it stale to keep my queue honest — push anything and I'll take the label straight back off.

Ping me if the cherry-pick fights you and I'll help sort it.

@Abhash-Chakraborty Abhash-Chakraborty added the stale Marked stale; auto-closed 7 days after labeling if not addressed. label Jul 30, 2026
@github-actions

Copy link
Copy Markdown

This PR is marked as stale. It will close automatically after 7 days without renewed activity; remove the stale label when work resumes.

@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Solid work — the allowlist-first redaction approach is exactly right, and 24 tests on it is the kind of coverage this needs. But this is targeting main, which is why the Branch policy check is red; everything goes to canary first. Please retarget the PR (and rebase onto canary, its moved a fair bit since mid-July).

Two things to fix while you are at it. ensure_error_log_buffer() runs at module import in routers/diagnostics.py — installing a global logging handler as an import side effect is going to bite us in tests and in the worker process. Move it into the app lifespan/startup in main.py. Second, collect_diagnostics_bundle() is a sync def doing PG/Redis/MinIO health probes, so a hung service will pin a threadpool worker for the full timeout; please make sure every probe has an explicit short timeout.

Retarget to canary and I will do a proper line-by-line pass on the redaction module.

- Move ensure_error_log_buffer() init from import-time side effect
  to app lifespan/startup in main.py, avoiding duplicate handler
  registration under uvicorn --reload
- Add explicit timeouts to PG/Redis/MinIO health probes in
  collect_diagnostics_bundle(); hangs now report ok: false instead
  of pinning a threadpool worker
- Document intentional over-redaction of substring matches
  (interface, pgvector, etc.) in _SENSITIVE_KEY_SUBSTRING_RE
- Add admin-only / local-only callout to top of
  docs/diagnostics-bundle.md
@Shiva210Jyoti
Shiva210Jyoti changed the base branch from main to canary August 3, 2026 10:40
@Shiva210Jyoti
Shiva210Jyoti changed the base branch from canary to main August 3, 2026 10:41
@Shiva210Jyoti

Copy link
Copy Markdown
Contributor Author

Recreated against canary as requested — see #428. Closing this one.

@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Superseded by #428, which is the same work targeting canary instead of main. Continuing review there — thanks for reopening it against the right base.

Abhash-Chakraborty added a commit that referenced this pull request Aug 3, 2026
* feat: generate privacy-safe local diagnostics bundle (#347)

- Add diagnostics/bundle.py to collect version, runtime, Alembic state, service health, queue stats, ML config, sanitized errors
- Add diagnostics/redact.py with allowlist-first recursive redaction
- Add GET /api/admin/diagnostics/bundle admin-only endpoint
- Add 24 passing redaction tests
- Add docs/diagnostics-bundle.md
- Wire router in main.py, link from docs/index.md
- Local JSON only, no uploads or telemetry

fix: address Copilot review and clear GitGuardian secrets in tests

- Use existing global engine in _collect_migration_state to avoid pool leaks
- Dedupe error log buffer handlers by name for uvicorn reload safety
- Add API tests covering headers, schema_version, auth (28 tests passing)
- Replace realistic-looking test fixtures with obvious placeholders

fix: address all review comments on diagnostics bundle PR #361

- Fix _URL_CREDS_RE to handle password-only URLs (redis://:pass@host)
- Rename sensitive dict keys to redacted_key instead of preserving name
- Scrub free-standing quoted strings via _QUOTED_CONTENT_RE
- Add lock to snapshot() for thread safety
- Extend filename scrub to any word.ext pattern
- Local storage check uses is_dir() + os.access W_OK
- Reuse global engine in _collect_migration_state
- Clarify docs: hostnames may appear, only URL credentials stripped
- Router returns scrubbed 500 JSON, no stack traces exposed
- 508 tests passing

fix: replace scanner-facing test fixtures to clear GitGuardian

- Replace JWT-shaped bearer token with FAKE.TEST.TOKEN
- Replace sk-live API key with split sk-test-FAKE-KEY-FOR-TESTING-ONLY
- Replace passwords with EXAMPLE_PASSWORD_PLACEHOLDER
- 26 tests passing

* fix: address remaining review comments on diagnostics bundle PR

bundle.py: guard error log buffer with threading.Lock in emit()/snapshot() to prevent deque mutation under concurrent logging. redact.py: generalize filename scrubbing regex to catch non-allowlisted extensions (.txt, .csv) and dotfiles (.env, .gitignore) while avoiding false positives on version strings. routers/diagnostics.py: stop leaking exception class/message to clients on failure; log full exception server-side via logger.exception() and return a generic error message. test_diagnostics_redact.py and test_diagnostics_api.py: add coverage for new redaction cases and the diagnostics endpoint (headers, payload shape, secret leakage, generic 500, shared-mode auth).

Addresses review feedback from CodeRabbit, Macroscope, GitHub Copilot, and CodeQL on PR #361.

* fix: address reload handler leak and token regex boundary bug

bundle.py: ensure_error_log_buffer now removes any stale same-named handler before attaching the current _error_buffer instance, fixing silent error-capture loss after uvicorn --reload. redact.py: fix _TOKEN_RE boundary so tokens ending in '-' are correctly redacted (previously leaked past the \b word boundary since '-' is a non-word character).

Adds test_strips_long_token_ending_in_hyphen regression test. Full suite: 561 passed, 5 skipped. Diagnostics: 34 passed.

Addresses Macroscope findings on PR #361.

* fix: address remaining review comments on diagnostics bundle PR

- Move ensure_error_log_buffer() init from import-time side effect
  to app lifespan/startup in main.py, avoiding duplicate handler
  registration under uvicorn --reload
- Add explicit timeouts to PG/Redis/MinIO health probes in
  collect_diagnostics_bundle(); hangs now report ok: false instead
  of pinning a threadpool worker
- Document intentional over-redaction of substring matches
  (interface, pgvector, etc.) in _SENSITIVE_KEY_SUBSTRING_RE
- Add admin-only / local-only callout to top of
  docs/diagnostics-bundle.md

* fix(diagnostics): make the probe timeout real and stop redacting useful fields

Three bugs found by running the collector rather than reading it. All three
passed the existing suite, so each fix ships with a test that fails without it.

1. The health-probe timeout never bounded anything. `_run_with_timeout` used
   `with ThreadPoolExecutor(...)`, and `Executor.__exit__` calls
   `shutdown(wait=True)` — so on a hung probe it raised TimeoutError and then
   blocked until the probe finished anyway. Measured: a 10s hang with a 1s
   timeout returned after 10.00s. Now shuts down with wait=False and abandons
   the thread, which is safe because every probe already sets its own socket
   timeout. Same hang now returns in 1.01s, and a full collection against a
   fully-down stack went 18.3s -> 8.6s.

2. `embedding_dim` was being destroyed. It is explicitly on ALLOWED_KEYS, but
   the substring deny-net matched "embedding" inside it and replaced the whole
   entry with `redacted_key: [REDACTED]`. The substring heuristic is a net for
   keys nobody vetted, so it no longer overrides curated allowlist entries;
   exact-match sensitive names still win over the allowlist.

3. Model names came out as `<filename>`. `yolo26n.pt` is filename-shaped, so
   the generic filename pattern collapsed `yolo_model` and every entry of
   `configured_models` — which is most of what the models section exists to
   report. Model-identifier keys are now exempt from filename scrubbing only;
   path, credential, token, and private-field scrubbing still apply to them,
   and the exemption is per-key so siblings are unaffected.

Also: several sensitive keys in one dict all wrote to the same `redacted_key`
entry, so all but the last silently vanished — they now get distinct
placeholders. And the migration-state and failed-media queries open real DB
connections but had no bound, so they are routed through the same timeout as
the health probes.

Tests: +12 covering each fix, including guards proving the two redaction
relaxations do not leak (credentials and absolute paths under an exempt key
are still scrubbed, non-allowlisted lookalikes are still denied), plus a real
collector-through-endpoint test asserting strict JSON serialisation — every
other endpoint test patches the collector out.

Full backend suite: 665 passed, 6 skipped. ruff check and format clean.

---------

Co-authored-by: Abhash Chakraborty <80592559+Abhash-Chakraborty@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend FastAPI, database, storage, and API work gssoc26 Related to GirlScript Summer of Code 2026. level:advanced GSSoC difficulty level: advanced. Base contributor points: 55. privacy Data privacy, security boundaries, and user trust stale Marked stale; auto-closed 7 days after labeling if not addressed. type:feature Feature PR. GSSoC type bonus. under-review Maintainer needs to verify

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: generate a privacy-safe local diagnostics bundle

4 participants