mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-10 08:01:17 +00:00
177 lines
5.1 KiB
Python
177 lines
5.1 KiB
Python
# This module is almost entirely written by a chatbot, with heavy guidance in terms of desired outcome, but very little
|
|
# code review. It's smoke-tested against all sample events and char-for-char tested for a single representative event.
|
|
#
|
|
# Large parts mirror (have stolen from) existing stacktrace-rendering logic from our views/templates, trimmed down for a
|
|
# Markdown/LLM audience.
|
|
#
|
|
# Purpose: expose event stacktraces (frames, source, locals) as clean, low-maintenance text for humans and machine
|
|
# tools. As in the UI: focus on the stacktrace rather than the event metadata.
|
|
#
|
|
# The provided markdown is not a stable interface; it's intended to be useful but not something you'd parse
|
|
# programmatically (just use the event data instead).
|
|
|
|
|
|
from events.utils import apply_sourcemaps
|
|
|
|
|
|
def _code_segments(frame):
|
|
pre = frame.get("pre_context") or []
|
|
ctx = frame.get("context_line")
|
|
post = frame.get("post_context") or []
|
|
|
|
pre = [("" if l is None else str(l)) for l in pre]
|
|
post = [("" if l is None else str(l)) for l in post]
|
|
if ctx is not None:
|
|
ctx = str(ctx)
|
|
|
|
return pre, ctx, post
|
|
|
|
|
|
def _code_lines(frame):
|
|
pre, ctx, post = _code_segments(frame)
|
|
lines = []
|
|
lines.extend(pre)
|
|
if ctx is not None:
|
|
lines.append(ctx)
|
|
lines.extend(post)
|
|
return lines
|
|
|
|
|
|
def _iter_exceptions(parsed):
|
|
exc = parsed.get("exception")
|
|
if not exc:
|
|
return []
|
|
if isinstance(exc, dict):
|
|
return list(exc.get("values") or [])
|
|
if isinstance(exc, (list, tuple)):
|
|
return list(exc)
|
|
return []
|
|
|
|
|
|
def _frames_for_exception(exc):
|
|
st = exc.get("stacktrace") or {}
|
|
return list(st.get("frames") or [])
|
|
|
|
|
|
def _header_lines(event, exc):
|
|
etype = exc.get("type") or "Exception"
|
|
val = exc.get("value") or ""
|
|
# Two-line title; no platform/event_id/timestamp clutter.
|
|
return [f"# {etype}", val]
|
|
|
|
|
|
def _format_frame_header(frame):
|
|
fn = frame.get("filename") or frame.get("abs_path") or "<unknown>"
|
|
func = frame.get("function") or ""
|
|
lineno = frame.get("lineno")
|
|
in_app = frame.get("in_app") is True
|
|
scope = "in-app" if in_app else "external"
|
|
|
|
header = f"### {fn}"
|
|
if lineno is not None:
|
|
header += f":{lineno}"
|
|
if func:
|
|
header += f" in `{func}`"
|
|
header += f" [{scope}]"
|
|
|
|
debug_id = frame.get("debug_id")
|
|
if debug_id and not frame.get("mapped"):
|
|
header += f" (no sourcemap for debug_id {debug_id})"
|
|
return [header]
|
|
|
|
|
|
def _format_code_gutter(frame):
|
|
pre, ctx, post = _code_segments(frame)
|
|
if not pre and ctx is None and not post:
|
|
return []
|
|
|
|
lineno = frame.get("lineno")
|
|
if lineno is not None:
|
|
start = max(1, int(lineno) - len(pre))
|
|
else:
|
|
start = 1
|
|
|
|
lines = list(pre)
|
|
ctx_index = None
|
|
if ctx is not None:
|
|
ctx_index = len(lines)
|
|
lines.append(ctx)
|
|
lines.extend(post)
|
|
|
|
last_no = start + len(lines) - 1
|
|
width = max(2, len(str(last_no)))
|
|
|
|
out = []
|
|
for i, text in enumerate(lines):
|
|
n = start + i
|
|
if ctx_index is not None and i == ctx_index:
|
|
out.append(f"▶ {str(n).rjust(width)} | {text}")
|
|
else:
|
|
out.append(f" {str(n).rjust(width)} | {text}")
|
|
return out
|
|
|
|
|
|
def _format_locals(frame):
|
|
vars_ = frame.get("vars") or {}
|
|
if not vars_:
|
|
return []
|
|
lines = ["", "#### Locals", ""]
|
|
for k, v in vars_.items():
|
|
lines.append(f"* `{k}` = `{v}`")
|
|
return lines
|
|
|
|
|
|
def _select_frames(frames, in_app_only):
|
|
if not in_app_only:
|
|
return frames
|
|
filtered = [f for f in frames if f.get("in_app") is True]
|
|
return filtered if filtered else frames
|
|
|
|
|
|
def render_stacktrace_md(event, frames="in_app", include_locals=True):
|
|
parsed = event.get_parsed_data()
|
|
try:
|
|
apply_sourcemaps(parsed)
|
|
except Exception:
|
|
pass
|
|
|
|
excs = _iter_exceptions(parsed)
|
|
if not excs:
|
|
return "_No stacktrace available._"
|
|
|
|
stack_of_plates = getattr(event, "platform", None) != "python"
|
|
if stack_of_plates:
|
|
excs = list(reversed(excs))
|
|
|
|
lines = []
|
|
for i, exc in enumerate(excs):
|
|
if i > 0:
|
|
lines += ["", "**During handling of the above exception, another exception occurred:**", ""]
|
|
lines += _header_lines(event, exc)
|
|
|
|
frames_list = _frames_for_exception(exc) or []
|
|
if stack_of_plates and frames_list:
|
|
frames_list = list(reversed(frames_list))
|
|
|
|
in_app_only = frames == "in_app"
|
|
frames_list = _select_frames(frames_list, in_app_only)
|
|
|
|
for frame in frames_list:
|
|
# spacer above every frame header
|
|
lines.append("")
|
|
lines += _format_frame_header(frame)
|
|
|
|
code_listing = _format_code_gutter(frame)
|
|
if code_listing:
|
|
lines += code_listing
|
|
else:
|
|
# brief mention when no source context is available
|
|
lines.append("_no source context available_")
|
|
|
|
if include_locals:
|
|
loc_lines = _format_locals(frame)
|
|
if loc_lines:
|
|
lines += loc_lines
|
|
|
|
return "\n".join(lines).strip()
|