Post Discord bug reports as plain text with log attachment

Discord-bound bug reports now put the description and metadata in the
message content and attach the full recent logs as recent_logs.txt,
instead of shipping everything inside a webhook embed. Webhook-embed
bodies aren't surfaced by the Claude Code Discord plugin's
fetch_messages/push path, so reports couldn't be auto-processed;
plain-text content and a file attachment both flow through cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 16:36:30 -07:00
co-authored by Claude Opus 4.6
parent c340f2b79d
commit 228101b937
@@ -296,15 +296,16 @@ namespace common.GUIUtils {
webhookUrl.Contains("discord.com") || webhookUrl.Contains("discordapp.com");
bool isSlack = webhookUrl.Contains("slack.com");
// Use multipart form for Discord with screenshot
if (isDiscord && _capturedScreenshot != null) {
return await SendDiscordWithScreenshot(webhookUrl, report, _capturedScreenshot);
// Discord always uses multipart: logs go as a file attachment so
// the full text is preserved, and the main content is plain text
// (not an embed) so the Claude Code Discord plugin can read it
// from msg.content. Screenshot is included if the user captured one.
if (isDiscord) {
return await SendDiscordMultipart(webhookUrl, report, _capturedScreenshot);
}
string json;
if (isDiscord) {
json = BuildDiscordPayload(report);
} else if (isSlack) {
if (isSlack) {
json = BuildSlackPayload(report);
} else {
// Generic webhook - use simple JSON
@@ -331,13 +332,27 @@ namespace common.GUIUtils {
}
private async Task<bool>
SendDiscordWithScreenshot(string webhookUrl, BugReport report, byte[] screenshot) {
SendDiscordMultipart(string webhookUrl, BugReport report, byte[] screenshot) {
var form = new List<IMultipartFormSection>();
// Logs as a file attachment so we don't lose content to Discord's
// 2000-character message-content cap.
var logsText =
string.IsNullOrEmpty(report.RecentLogs) ? "(no logs)" : report.RecentLogs;
form.Add(new MultipartFormFileSection(
"file",
screenshot,
"screenshot.png",
"image/png"));
"logs",
Encoding.UTF8.GetBytes(logsText),
"recent_logs.txt",
"text/plain"));
if (screenshot != null) {
form.Add(new MultipartFormFileSection(
"screenshot",
screenshot,
"screenshot.png",
"image/png"));
}
form.Add(new MultipartFormDataSection("payload_json", BuildDiscordPayload(report)));
using var request = UnityWebRequest.Post(webhookUrl, form);
@@ -346,7 +361,7 @@ namespace common.GUIUtils {
while (!operation.isDone) { await Task.Yield(); }
if (request.result != UnityWebRequest.Result.Success) {
Debug.LogError($"Discord screenshot upload failed: {request.error}");
Debug.LogError($"Discord bug report upload failed: {request.error}");
return false;
}
@@ -354,44 +369,42 @@ namespace common.GUIUtils {
}
private string BuildDiscordPayload(BugReport report) {
// Discord webhook format with embeds for nice formatting
var truncatedLogs = TruncateForDiscord(report.RecentLogs, 1000);
// Plain-text content (not an embed) so the Claude Code Discord plugin
// can read it from msg.content. Full logs ride along as a file
// attachment, not inline, so content stays inside Discord's 2000-char
// cap even on long reports.
var content = new StringBuilder();
content.Append("**Bug Report** from Eagle0 client\n\n");
content.Append($"**Description:** {report.Description}\n\n");
content.Append($"**Build:** {report.BuildVersion} ({report.BuildTimestamp})\n");
content.Append($"**Platform:** {report.Platform}\n");
content.Append($"**Time:** {report.Timestamp:yyyy-MM-dd HH:mm:ss} UTC\n\n");
var gameInfo = new StringBuilder();
if (report.GameId.HasValue) gameInfo.AppendLine($"Game ID: {report.GameId}");
if (report.FactionId.HasValue) gameInfo.AppendLine($"Faction ID: {report.FactionId}");
content.Append("**Game State:**\n");
if (report.GameId.HasValue) content.Append($"- Game ID: {report.GameId}\n");
if (report.FactionId.HasValue) content.Append($"- Faction ID: {report.FactionId}\n");
if (!string.IsNullOrEmpty(report.CurrentDate))
gameInfo.AppendLine($"Game Date: {report.CurrentDate}");
content.Append($"- Game Date: {report.CurrentDate}\n");
if (report.ActiveBattles?.Any() == true)
gameInfo.AppendLine($"Active Battles: {string.Join(", ", report.ActiveBattles)}");
content.Append($"- Active Battles: {string.Join(", ", report.ActiveBattles)}\n");
if (!report.GameId.HasValue) content.Append("- Not in a game\n");
var gameInfoStr = gameInfo.Length > 0 ? gameInfo.ToString() : "Not in a game";
content.Append($"\n**System Info:**\n```\n{report.SystemInfo}```\n");
content.Append("\nFull logs attached.");
// Build JSON using StringBuilder to avoid clang-format issues with verbatim strings
var sb = new StringBuilder();
sb.Append("{");
sb.Append("\"content\": \"**Bug Report** from Eagle0 client\",");
sb.Append("\"embeds\": [{");
sb.Append("\"title\": \"Bug Report\",");
sb.Append($"\"description\": \"{EscapeJson(report.Description)}\",");
sb.Append("\"color\": 15158332,");
sb.Append("\"fields\": [");
sb.Append(
$"{{\"name\": \"Build\", \"value\": \"{EscapeJson(report.BuildVersion)} ({EscapeJson(report.BuildTimestamp)})\", \"inline\": true}},");
sb.Append(
$"{{\"name\": \"Platform\", \"value\": \"{EscapeJson(report.Platform)}\", \"inline\": true}},");
sb.Append(
$"{{\"name\": \"Timestamp\", \"value\": \"{report.Timestamp:yyyy-MM-dd HH:mm:ss} UTC\", \"inline\": true}},");
sb.Append(
$"{{\"name\": \"Game State\", \"value\": \"{EscapeJson(gameInfoStr)}\", \"inline\": false}},");
sb.Append(
$"{{\"name\": \"System Info\", \"value\": \"```{EscapeJson(report.SystemInfo)}```\", \"inline\": false}},");
sb.Append(
$"{{\"name\": \"Recent Logs\", \"value\": \"```{EscapeJson(truncatedLogs)}```\", \"inline\": false}}");
sb.Append("],");
sb.Append($"\"timestamp\": \"{report.Timestamp:yyyy-MM-ddTHH:mm:ssZ}\"");
sb.Append("}]}");
return sb.ToString();
// Discord message content cap is 2000 chars — truncate the tail if
// the description + metadata somehow blow past that.
const int MaxContent = 1990;
var contentStr = content.ToString();
if (contentStr.Length > MaxContent) {
contentStr = contentStr.Substring(0, MaxContent - 3) + "...";
}
var json = new StringBuilder();
json.Append("{\"content\": \"");
json.Append(EscapeJson(contentStr));
json.Append("\"}");
return json.ToString();
}
private string BuildSlackPayload(BugReport report) {