Files
Thales 7d0ef12302 Rework the notification centre's failure report: fix the Windows Explorer
bug, add Discord, full traceback, opt-in logs, and anonymization

Root cause of the Explorer bug: the pre-filled GitHub URL carried the full
diagnostic dump (up to 6000 chars) as a query param, and Windows opens it via
explorer.exe, which silently falls back to a plain File Explorer window past
roughly 2000 characters instead of erroring. buildReportUrl() now fills the
"Logs / screenshots" field directly with as much of the traceback/stderr
tail as fits (keeping the end, where the actual error is - no paste needed
for the common case), and only points at the clipboard for what doesn't fit.
buildReportText() always has the complete, untruncated version.

Also added:
- A second "Report on Discord" button next to "Report on GitHub".
- Full backend traceback capture (_quarantine_failed_job), not just a
  one-line exception repr - fixed a latent bug in the same change where the
  tail parser would have silently swallowed a second section into the first.
- An opt-in "Include recent logs" button pulling from the backend/
  application/setup log views already exposed by Settings -> Logs, scoped to
  a window around the failure's own timestamp.
- Anonymization (app/core/redact.py): strips the reporter's home directory,
  any YouTube/SoundCloud source URL (download.py logs every job's URL, not
  just the failing one - a raw log tail would otherwise leak everything
  imported in the fetched window), and any IPv4 address (the mobile UI talks
  to this backend over the LAN). Applied unconditionally in GET
  /api/logs/{view}, not just for the report flow, and to the per-job
  traceback/tail/exception before error.txt is ever written. title:/source:
  stay unredacted in that file on purpose - they're already excluded from
  the public API response, so redacting them there loses local diagnostic
  value for no privacy gain.

Closes #381, #384
2026-08-17 17:47:07 +01:00

76 lines
2.6 KiB
Python

from __future__ import annotations
from pathlib import Path
from app.core.redact import redact
def test_strips_the_home_directory():
home = str(Path.home())
text = f"{home}\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\threads.py"
result = redact(text)
assert home not in result
assert "<home>" in result
assert "threads.py" in result
def test_strips_a_youtube_url():
text = "download starting: https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ"
result = redact(text)
assert "youtube.com" not in result
assert "dQw4w9WgXcQ" not in result
assert "<source-url-redacted>" in result
def test_strips_a_youtu_be_url():
result = redact("source: https://youtu.be/dQw4w9WgXcQ")
assert "youtu.be" not in result
assert "<source-url-redacted>" in result
def test_strips_a_soundcloud_url():
result = redact("source: https://soundcloud.com/artist/track")
assert "soundcloud.com" not in result
assert "<source-url-redacted>" in result
def test_does_not_touch_an_unrelated_url():
"""Only the hosts this app actually pulls tracks from are source URLs --
a link to the app's own site/repo, or anything else, is not personal
information and must survive (it's often the useful part of a log line)."""
text = "see https://github.com/stemdeckapp/stemdeck/issues/277 and https://stemdeck.app"
result = redact(text)
assert result == text
def test_strips_an_ipv4_address():
result = redact('192.168.1.14:52341 - "GET /api/jobs HTTP/1.1" 200 OK')
assert "192.168.1.14" not in result
assert "<ip>" in result
def test_does_not_mistake_a_version_string_for_an_ip():
result = redact("StemDeck v0.9.1.dev2+g682ab90d7.d20260816")
assert "0.9.1" in result
assert "<ip>" not in result
def test_does_not_mistake_a_timestamp_for_anything():
"""The pipeline's own log format is `YYYY-MM-DD HH:MM:SS ...` -- every
single line has one, so any redaction pattern with false positives here
would mangle the entire report, not just the rare real leak."""
text = "2026-08-17 16:50:02 E stemdeck.pipeline pipeline failed for job abcdefabcdef"
assert redact(text) == text
def test_composes_all_three_kinds_in_one_pass():
home = str(Path.home())
text = f"{home}\\stemdeck 192.168.1.14 requested https://www.youtube.com/watch?v=dQw4w9WgXcQ"
result = redact(text)
assert home not in result
assert "192.168.1.14" not in result
assert "youtube.com" not in result
assert "<home>" in result
assert "<ip>" in result
assert "<source-url-redacted>" in result