feat: augment outage report template with debugging context
- Expand service list: add Home Assistant, Actual Budget, Audiobookshelf, Linkwarden, Matrix, Paperless, Tandoor, FreshRSS, Frigate, HackMD, Excalidraw, Wealthfolio, Send, Stirling PDF - Add structured debugging fields: error type, scope (just me vs others), when it started, URL accessed - Fix user report parser to extract all form fields into status.json - Show error type, scope, and start time in status page report cards [ci skip] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c00a908610
commit
3e9231ae0d
3 changed files with 95 additions and 20 deletions
71
.github/ISSUE_TEMPLATE/outage-report.yml
vendored
71
.github/ISSUE_TEMPLATE/outage-report.yml
vendored
|
|
@ -8,16 +8,30 @@ body:
|
||||||
label: Affected Service
|
label: Affected Service
|
||||||
description: Which service is affected?
|
description: Which service is affected?
|
||||||
options:
|
options:
|
||||||
- Nextcloud
|
- Nextcloud (files, calendar, contacts)
|
||||||
- Immich
|
- Immich (photos)
|
||||||
- Vaultwarden
|
- Vaultwarden (passwords)
|
||||||
- Grafana
|
- Mail (email, roundcube)
|
||||||
|
- Home Assistant
|
||||||
|
- Actual Budget
|
||||||
|
- Navidrome (music)
|
||||||
|
- Audiobookshelf (audiobooks)
|
||||||
- Plex / Jellyfin
|
- Plex / Jellyfin
|
||||||
- Mail
|
- Grafana (dashboards)
|
||||||
|
- Linkwarden (bookmarks)
|
||||||
|
- Matrix (chat)
|
||||||
|
- Paperless-ngx (documents)
|
||||||
|
- Tandoor (recipes)
|
||||||
|
- FreshRSS (news)
|
||||||
|
- Frigate (cameras)
|
||||||
|
- HackMD (notes)
|
||||||
|
- Excalidraw (whiteboard)
|
||||||
|
- Wealthfolio / Finance
|
||||||
|
- Headscale / VPN
|
||||||
- DNS
|
- DNS
|
||||||
- VPN / Tailscale
|
|
||||||
- Website / Blog
|
- Website / Blog
|
||||||
- Music (Navidrome / Freedify)
|
- Send (file sharing)
|
||||||
|
- Stirling PDF
|
||||||
- Other
|
- Other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -29,6 +43,49 @@ body:
|
||||||
placeholder: "e.g., Getting 502 errors when trying to access Nextcloud since about 3pm"
|
placeholder: "e.g., Getting 502 errors when trying to access Nextcloud since about 3pm"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: error_type
|
||||||
|
attributes:
|
||||||
|
label: What kind of error?
|
||||||
|
description: This helps us narrow down the issue faster.
|
||||||
|
options:
|
||||||
|
- Page won't load (timeout / connection refused)
|
||||||
|
- 502 Bad Gateway
|
||||||
|
- 503 Service Unavailable
|
||||||
|
- Login / authentication not working
|
||||||
|
- Slow / degraded performance
|
||||||
|
- Specific feature broken (app loads but something inside doesn't work)
|
||||||
|
- Data missing or incorrect
|
||||||
|
- Other / not sure
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: scope
|
||||||
|
attributes:
|
||||||
|
label: Is it just you or others too?
|
||||||
|
description: Helps us tell apart service outages from account/device issues.
|
||||||
|
options:
|
||||||
|
- Just me (others seem fine)
|
||||||
|
- Multiple people affected
|
||||||
|
- Not sure
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: when
|
||||||
|
attributes:
|
||||||
|
label: When did it start?
|
||||||
|
description: Approximate time helps us correlate with logs and deployments.
|
||||||
|
placeholder: "e.g., about 3pm today, or since yesterday morning"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: url
|
||||||
|
attributes:
|
||||||
|
label: URL you were accessing (optional)
|
||||||
|
description: The exact URL helps us check the right endpoint.
|
||||||
|
placeholder: "e.g., https://nextcloud.viktorbarzin.me/apps/files"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
- type: input
|
- type: input
|
||||||
id: contact
|
id: contact
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,9 @@ footer { color: var(--fg3); font-size: 11px; margin-top: 32px; padding-top: 16px
|
||||||
html+='<div class="inc-info"><div class="inc-title">'+esc(inc.title)+'</div>';
|
html+='<div class="inc-info"><div class="inc-title">'+esc(inc.title)+'</div>';
|
||||||
html+='<div class="inc-meta"><span>'+ago(created)+'</span>';
|
html+='<div class="inc-meta"><span>'+ago(created)+'</span>';
|
||||||
if(!isReport)html+='<span>'+dur(created,end)+'</span>';
|
if(!isReport)html+='<span>'+dur(created,end)+'</span>';
|
||||||
|
if(isReport&&inc.error_type)html+='<span>'+esc(inc.error_type)+'</span>';
|
||||||
|
if(isReport&&inc.scope)html+='<span>'+esc(inc.scope)+'</span>';
|
||||||
|
if(isReport&&inc.when_started)html+='<span>Since: '+esc(inc.when_started)+'</span>';
|
||||||
if(resolved)html+='<span style="color:var(--green)">Resolved</span>';
|
if(resolved)html+='<span style="color:var(--green)">Resolved</span>';
|
||||||
html+='</div>';
|
html+='</div>';
|
||||||
if(inc.affected_services&&inc.affected_services.length){
|
if(inc.affected_services&&inc.affected_services.length){
|
||||||
|
|
|
||||||
|
|
@ -385,22 +385,33 @@ ISSUES_REPO = "ViktorBarzin/infra"
|
||||||
def has_label(issue, name):
|
def has_label(issue, name):
|
||||||
return any(l["name"].lower() == name.lower() for l in issue.get("labels", []))
|
return any(l["name"].lower() == name.lower() for l in issue.get("labels", []))
|
||||||
|
|
||||||
def parse_user_report_service(body):
|
def parse_form_field(body, heading):
|
||||||
"""Extract service from GitHub Issue Form dropdown response."""
|
"""Extract value after a ### heading from GitHub Issue Form response."""
|
||||||
if not body:
|
if not body:
|
||||||
return None
|
return None
|
||||||
for line in body.split("\n"):
|
lines = body.split("\n")
|
||||||
stripped = line.strip()
|
for i, ln in enumerate(lines):
|
||||||
if stripped and not stripped.startswith("#") and not stripped.startswith("_") and not stripped.startswith("<!"):
|
if heading.lower() in ln.lower() and ln.strip().startswith("#"):
|
||||||
prev_was_heading = False
|
for j in range(i + 1, len(lines)):
|
||||||
for i, ln in enumerate(body.split("\n")):
|
val = lines[j].strip()
|
||||||
if "affected service" in ln.lower():
|
if val and not val.startswith("#") and val != "_No response_":
|
||||||
prev_was_heading = True
|
return val
|
||||||
continue
|
|
||||||
if prev_was_heading and ln.strip():
|
|
||||||
return ln.strip()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def parse_user_report_context(body):
|
||||||
|
"""Extract structured fields from the issue form body."""
|
||||||
|
service = parse_form_field(body, "affected service")
|
||||||
|
# Strip parenthetical hints: "Nextcloud (files, calendar)" -> "Nextcloud"
|
||||||
|
if service and "(" in service:
|
||||||
|
service = service[:service.index("(")].strip()
|
||||||
|
return {
|
||||||
|
"service": service,
|
||||||
|
"error_type": parse_form_field(body, "what kind of error"),
|
||||||
|
"scope": parse_form_field(body, "is it just you"),
|
||||||
|
"when": parse_form_field(body, "when did it start"),
|
||||||
|
"url": parse_form_field(body, "url you were accessing"),
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
issues_url = "https://api.github.com/repos/" + ISSUES_REPO + "/issues"
|
issues_url = "https://api.github.com/repos/" + ISSUES_REPO + "/issues"
|
||||||
|
|
||||||
|
|
@ -435,7 +446,8 @@ try:
|
||||||
continue
|
continue
|
||||||
if has_label(issue, "incident"):
|
if has_label(issue, "incident"):
|
||||||
continue # Already promoted to incident, skip duplicate
|
continue # Already promoted to incident, skip duplicate
|
||||||
svc = parse_user_report_service(issue.get("body"))
|
ctx = parse_user_report_context(issue.get("body"))
|
||||||
|
svc = ctx["service"]
|
||||||
user_reports.append({
|
user_reports.append({
|
||||||
"id": issue["number"],
|
"id": issue["number"],
|
||||||
"title": issue["title"],
|
"title": issue["title"],
|
||||||
|
|
@ -443,6 +455,9 @@ try:
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"created_at": issue["created_at"],
|
"created_at": issue["created_at"],
|
||||||
"affected_services": [svc] if svc else [],
|
"affected_services": [svc] if svc else [],
|
||||||
|
"error_type": ctx.get("error_type"),
|
||||||
|
"scope": ctx.get("scope"),
|
||||||
|
"when_started": ctx.get("when"),
|
||||||
"url": issue["html_url"],
|
"url": issue["html_url"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue