Bug Report tool

Unity In-Game Bug Report

I made a bug report script that sends bug information from the game to a discord server. Its a simple C# script in Unity that gathers the data needed and sends the info via a WWWForm.

I also created a small Dicord server bot that checks for incoming messages on the dedicated channel and then creates interactive buttons for the bug report. The bot also recreates the bug report and creates a separate channel under a different category. The buttons created, allow for the developers to indicate how far along the bug is and whether they need more information. Once the DONE button is clicked the bug report is moved to a different category and removed from the old one. So that it can be seen as completed and archived.


Below is a short video showing the bug report script.

Below is the code used in Unity and C#.

You can checkout the Discord bot code on GitHub. The bot is running using Railway.


            using System;
            using System.IO;
            using System.Collections;
            using System.Text;
            using UnityEngine;
            using UnityEngine.UI;
            using UnityEngine.Networking;
            using TMPro;
            using SFB;
            using System.IO.Compression;
            using System.Collections.Generic;


            namespace DangryGames
            {
                public class BugReporter : MonoBehaviour
                {
                    [Header("UI References")]
                    [SerializeField] private TMP_InputField descriptionField;
                    [SerializeField] private TMP_InputField stepsField;
                    [SerializeField] private Toggle includeScreenshotToggle;
                    [SerializeField] private RawImage screenshotPreview;
                    [SerializeField] private GameObject feedbackPanel; 
                    [SerializeField] private TMP_Dropdown dropdownType;
                    [SerializeField] private TMP_Dropdown dropdownSeverity;

                    [Header("Discord Settings")]
                    [SerializeField] private string discordWebhookUrl = "YOUR_DISCORD_WEBHOOK_URL_HERE";

                    [Header("Reporter Settings")]
                    [SerializeField] private float reportCooldown = 30f; 
                    private float _lastReportTime = -999f;
                    private bool _isSending = false;

                    private Texture2D _selectedImageTexture;
                    private byte[] _selectedImageBytes;

                    private void Start()
                    {
                        string path = GetPlayerLogPath();
                        Debug.Log("Log path resolved to: " + path);
                        List options = new List { "Hard Crash", "SoftLock", "Framerate", "Audio", "Visual", "UI", "Gameplay", "Text", "Controls", "Resolution" };
                        dropdownType.AddOptions(options);
                        List sevOptions = new List {"High", "Medium", "Low"};
                        dropdownSeverity.AddOptions(sevOptions);
                    }

                    public void OpenImagePicker()
                    {
            #if UNITY_STANDALONE || UNITY_EDITOR
                        var extensions = new[]
                        {
                            new ExtensionFilter("Image Files", "png", "jpg", "jpeg")
                        };

                        string[] paths = StandaloneFileBrowser.OpenFilePanel("Select Screenshot", "", extensions, false);

                        if (paths == null || paths.Length == 0 || string.IsNullOrEmpty(paths[0]))
                            return;

                        StartCoroutine(LoadImagePreview(paths[0]));
            #else
                        Debug.LogWarning("File picker is only supported on standalone platforms.");
            #endif
                    }

                    private IEnumerator LoadImagePreview(string filePath)
                    {
                        if (!File.Exists(filePath))
                        {
                            Debug.LogError("Selected file does not exist: " + filePath);
                            yield break;
                        }

                        byte[] fileData = File.ReadAllBytes(filePath);
                        _selectedImageBytes = fileData;

                        if (_selectedImageTexture != null)
                        {
                            Destroy(_selectedImageTexture);
                            _selectedImageTexture = null;
                        }

                        _selectedImageTexture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
                        if (_selectedImageTexture.LoadImage(fileData))
                        {
                            screenshotPreview.texture = _selectedImageTexture;
                            screenshotPreview.gameObject.SetActive(true);
                            Debug.Log("Loaded screenshot preview from: " + filePath);
                        }
                        else
                        {
                            Debug.LogError("Failed to load image from: " + filePath);
                        }

                        yield return null;
                    }

                    private byte[] ZipBytes(string fileName, byte[] fileData)
                    {
                        using (var zipStream = new MemoryStream())
                        {
                            using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
                            {
                                var entry = archive.CreateEntry(fileName, System.IO.Compression.CompressionLevel.Optimal);

                                using (var entryStream = entry.Open())
                                {
                                    entryStream.Write(fileData, 0, fileData.Length);
                                }
                            }

                            return zipStream.ToArray();
                        }
                    }

                    public void SubmitBugReport()
                    {
                        if (_isSending)
                        {
                            Debug.LogWarning("Bug report already in progress.");
                            return;
                        }

                        if (Time.time - _lastReportTime < reportCooldown)
                        {
                            float remaining = reportCooldown - (Time.time - _lastReportTime);
                            Debug.LogWarning($"Please wait {remaining:F1} seconds before sending another bug report.");
                            return;
                        }

                        if (string.IsNullOrWhiteSpace(descriptionField.text))
                        {
                            Debug.LogWarning("Bug description is empty. Ask the player to fill it in.");
                            
                            return;
                        }

                        StartCoroutine(SendBugCoroutine());
                    }

                    private IEnumerator SendBugCoroutine()
                    {
                        _isSending = true;
                        _lastReportTime = Time.time;

                        string bugId = Guid.NewGuid().ToString().Substring(0, 8).ToUpper();
                        string description = descriptionField.text;
                        string steps = stepsField != null ? stepsField.text : string.Empty;

                        byte[] logBytes = null;
                        string logPath = GetPlayerLogPath();

                        if (!string.IsNullOrEmpty(logPath) && File.Exists(logPath))
                        {
                            try
                            {
                                using (FileStream fs = new FileStream(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                                {
                                    using (MemoryStream ms = new MemoryStream())
                                    {
                                        fs.CopyTo(ms);
                                        logBytes = ms.ToArray();
                                    }
                                }

                                Debug.Log("Loaded Editor/Player log (" + logBytes.Length + " bytes)");
                            }
                            catch (Exception e)
                            {
                                Debug.LogError("Failed reading log file: " + e.Message);
                            }
                        }
                        else
                        {
                            Debug.LogWarning("Log path not found: " + logPath);
                        }


                        string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
                        string platform = Application.platform.ToString();
                        string version = Application.version;
                        string timeStamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                        string bugType = $"{dropdownType.options[dropdownType.value].text}";
                        string bugSeverity = $"{dropdownSeverity.options[dropdownSeverity.value].text}";

                        string content =
                            $"**🐞 Bug Report — ID: `{bugId}`**\n" +
                            $"**Description:** {description}\n" +
                            $"**Steps:** {(string.IsNullOrWhiteSpace(steps) ? "_(none provided)_" : steps)}\n" +
                            $"**Scene:** {sceneName}\n" +
                            $"**Platform:** {platform}\n" +
                            $"**Bug Type:** {bugType}\n" +
                            $"**Severity:** {bugSeverity}\n" +
                            $"**Version:** {version}\n" +
                            $"**Time:** {timeStamp}";

                        WWWForm form = new WWWForm();
                        form.AddField("content", content);

                        if (includeScreenshotToggle != null && includeScreenshotToggle.isOn && _selectedImageBytes != null)
                        {
                            form.AddBinaryData("file", _selectedImageBytes, $"Bug_{bugId}_Screenshot.png", "image/png");
                        }

                        if (logBytes != null && logBytes.Length > 0)
                        {
                            byte[] zipped = ZipBytes($"Bug_{bugId}_PlayerLog.txt", logBytes);

                            form.AddBinaryData(
                                "logfile",
                                zipped,
                                $"Bug_{bugId}_PlayerLog.zip",
                                "application/zip"
                            );

                            Debug.Log("Zipped log size: " + zipped.Length + " bytes");
                        }


                        using (UnityWebRequest request = UnityWebRequest.Post(discordWebhookUrl, form))
                        {
                            yield return request.SendWebRequest();

            #if UNITY_2020_1_OR_NEWER
                            if (request.result == UnityWebRequest.Result.Success)
            #else
                            if (!request.isHttpError && !request.isNetworkError)
            #endif
                            {
                                Debug.Log($"Bug {bugId} sent to Discord successfully.");

                                if (feedbackPanel != null)
                                    feedbackPanel.SetActive(true);

                                OpenCodex op = feedbackPanel.GetComponent();
                                op.Open();

                                descriptionField.text = string.Empty;
                                if (stepsField != null) stepsField.text = string.Empty;

                                if (_selectedImageTexture != null)
                                {
                                    Destroy(_selectedImageTexture);
                                    _selectedImageTexture = null;
                                }
                                _selectedImageBytes = null;
                                if (screenshotPreview != null)
                                {
                                    screenshotPreview.texture = null;
                                    screenshotPreview.gameObject.SetActive(false);
                                }

                                OpenCodex reporterCanvas = GetComponent();
                                reporterCanvas.Hide();
                            }
                            else
                            {
                                Debug.LogError("Failed to send bug report to Discord: " + request.error);
                            }
                        }

                        _isSending = false;
                    }

                    private string GetPlayerLogPath()
                    {
            #if UNITY_EDITOR
                        return Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                            "Unity", "Editor", "Editor.log"
                        );

            #elif UNITY_STANDALONE_WIN
                string company = Application.companyName;
                string product = Application.productName;

                string path = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                    company, product, "Player.log"
                );

                if (File.Exists(path))
                    return path;

                // Fallback legacy
                return Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                    "Unity", "Player.log"
                );

            #elif UNITY_STANDALONE_OSX
                string personal = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
                return Path.Combine(
                    personal,
                    "Library/Logs",
                    Application.companyName,
                    Application.productName,
                    "Player.log"
                );

            #else
                // Final fallback for all other platforms
                return Path.Combine(Application.persistentDataPath, "Player.log");
            #endif
                    }


                }
            }