Quick answer: Use ScreenCapture.CaptureScreenshotAsTexture() to grab the current frame as a Texture2D, then encode it to PNG with texture.EncodeToPNG(). This must be called from a coroutine that yields WaitForEndOfFrame to ensure the frame is fully rendered.
Learning how to add bug reporter to Unity game is a common challenge for game developers. Players encounter bugs at the worst possible moments — mid-boss fight, during a cutscene, right after a long exploration session. By the time they alt-tab to a browser, find your bug tracker, and try to describe what happened, the context is gone. An in-game bug reporter captures that context immediately: a screenshot of the broken state, the player’s exact position, their hardware specs, and their description while the experience is fresh. This guide walks through building a complete in-game bug reporter in Unity, from the UI Canvas to HTTP submission to offline queuing.
Setting Up the UI Canvas
The bug reporter UI needs to be accessible from anywhere in the game without interfering with gameplay. Create a dedicated Canvas set to Screen Space – Overlay with a sort order of 100 or higher so it renders above everything else, including other UI elements.
Start by creating the Canvas hierarchy in your scene or as a prefab that persists across scene loads:
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class BugReporterUI : MonoBehaviour
{
[SerializeField] private GameObject _panel;
[SerializeField] private TMP_InputField _titleField;
[SerializeField] private TMP_InputField _descriptionField;
[SerializeField] private TMP_Dropdown _categoryDropdown;
[SerializeField] private Button _submitButton;
[SerializeField] private Button _cancelButton;
[SerializeField] private RawImage _screenshotPreview;
private BugReporter _reporter;
private byte[] _screenshotData;
void Awake()
{
_reporter = new BugReporter();
_panel.SetActive(false);
_submitButton.onClick.AddListener(OnSubmit);
_cancelButton.onClick.AddListener(OnCancel);
// Populate category dropdown
_categoryDropdown.ClearOptions();
_categoryDropdown.AddOptions(new List<string> {
"Gameplay", "Visual", "Audio",
"UI", "Performance", "Crash"
});
}
void Update()
{
// Toggle with F8 key
if (Input.GetKeyDown(KeyCode.F8))
{
if (_panel.activeSelf)
OnCancel();
else
StartCoroutine(OpenReporter());
}
}
}
The Canvas should contain a semi-transparent background panel, a title input field with placeholder text like “Brief description of the issue,” a multi-line description field, a category dropdown, a preview area for the screenshot, and Submit and Cancel buttons. Keep the form minimal — players will abandon a long form. Five fields is the maximum before submission rates drop.
Use DontDestroyOnLoad on the reporter’s root GameObject so it persists across scene changes. Players should be able to report a bug from any scene without losing access to the reporter.
Capturing a Screenshot
The screenshot is the most valuable piece of a bug report. It shows exactly what the player saw when the bug occurred. Capture it before showing the reporter UI so the screenshot shows the game state, not the report form.
using System.Collections;
using UnityEngine;
public partial class BugReporterUI
{
private IEnumerator OpenReporter()
{
// Wait for end of frame to capture the rendered frame
yield return new WaitForEndOfFrame();
// Capture screenshot before showing UI
Texture2D screenshot = ScreenCapture.CaptureScreenshotAsTexture();
// Encode to PNG for submission
_screenshotData = screenshot.EncodeToPNG();
// Show preview (downscaled for the UI)
_screenshotPreview.texture = screenshot;
// Now show the reporter panel
_panel.SetActive(true);
Time.timeScale = 0f; // Pause the game
// Clear previous input
_titleField.text = "";
_descriptionField.text = "";
_categoryDropdown.value = 0;
}
private void OnCancel()
{
_panel.SetActive(false);
Time.timeScale = 1f;
_screenshotData = null;
}
}
A few important details. The WaitForEndOfFrame yield is essential — without it, you capture an incomplete frame. Pause the game with Time.timeScale = 0f so the player can fill out the form without enemies attacking them. Remember to restore the time scale when the reporter closes.
For mobile games or lower-end hardware, consider capturing at half resolution to reduce memory usage and upload size. A 960x540 screenshot is usually sufficient for identifying visual bugs.
Collecting System Information
System information helps you reproduce bugs on the right hardware configuration. Unity’s SystemInfo class provides everything you need without requiring the player to know their specs.
using UnityEngine;
using UnityEngine.SceneManagement;
public static class SystemInfoCollector
{
public static string Collect()
{
var info = new System.Text.StringBuilder();
// Game info
info.AppendLine($"Game Version: {Application.version}");
info.AppendLine($"Scene: {SceneManager.GetActiveScene().name}");
info.AppendLine($"Platform: {Application.platform}");
// System info
info.AppendLine($"OS: {SystemInfo.operatingSystem}");
info.AppendLine($"CPU: {SystemInfo.processorType}");
info.AppendLine($"CPU Cores: {SystemInfo.processorCount}");
info.AppendLine($"RAM: {SystemInfo.systemMemorySize} MB");
// GPU info
info.AppendLine($"GPU: {SystemInfo.graphicsDeviceName}");
info.AppendLine($"VRAM: {SystemInfo.graphicsMemorySize} MB");
info.AppendLine($"Graphics API: {SystemInfo.graphicsDeviceType}");
// Display
var res = Screen.currentResolution;
info.AppendLine($"Resolution: {res.width}x{res.height}@{res.refreshRateRatio}Hz");
info.AppendLine($"Fullscreen: {Screen.fullScreenMode}");
info.AppendLine($"Quality Level: {QualitySettings.names[QualitySettings.GetQualityLevel()]}");
return info.ToString();
}
}
Include the player’s position in world space if it is relevant to your game. For a 3D game, capture Camera.main.transform.position and the player character’s position. For a 2D game, capture the camera bounds and player coordinates. This information makes it dramatically easier to find the exact location where a visual bug occurs.
Also capture the last few lines of the Unity log. Use Application.logMessageReceived to maintain a rolling buffer of the most recent 50 log entries. Include warnings and errors — these often contain the technical clue that explains the bug.
Submitting Reports via HTTP
Send the bug report to your tracking backend using UnityWebRequest. Use a multipart form to include both the JSON report data and the screenshot file.
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
public class BugReporter
{
private const string API_URL = "https://api.bugnet.io/v1/bugs";
private const string API_KEY = "YOUR_PROJECT_API_KEY";
public IEnumerator Submit(string title, string description,
string category, byte[] screenshot,
System.Action<bool> onComplete)
{
var form = new WWWForm();
form.AddField("title", title);
form.AddField("description", description);
form.AddField("category", category);
form.AddField("system_info", SystemInfoCollector.Collect());
if (screenshot != null)
form.AddBinaryData("screenshot", screenshot,
"screenshot.png", "image/png");
var request = UnityWebRequest.Post(API_URL, form);
request.SetRequestHeader("X-API-Key", API_KEY);
request.timeout = 30;
yield return request.SendWebRequest();
bool success = request.result ==
UnityWebRequest.Result.Success;
if (!success)
{
Debug.LogWarning(
$"Bug report failed: {request.error}. Queuing locally.");
QueueLocally(title, description, category, screenshot);
}
onComplete?.Invoke(success);
}
}
Set a reasonable timeout (30 seconds). If the server is unreachable, do not block the player — queue the report locally and let them return to the game. Show a brief confirmation message: “Report submitted, thank you!” for success, or “Report saved. It will be sent when you are back online.” for queued reports.
Building an Offline Queue
Players do not always have internet access when they encounter bugs, especially on Steam Deck, laptops, or mobile devices. An offline queue ensures no reports are lost.
using System.IO;
using UnityEngine;
public partial class BugReporter
{
private string QueuePath =>
Path.Combine(Application.persistentDataPath, "bug_queue");
private void QueueLocally(string title, string description,
string category, byte[] screenshot)
{
Directory.CreateDirectory(QueuePath);
var id = System.Guid.NewGuid().ToString();
var reportPath = Path.Combine(QueuePath, id);
Directory.CreateDirectory(reportPath);
// Save report metadata
var report = new QueuedReport
{
title = title,
description = description,
category = category,
systemInfo = SystemInfoCollector.Collect(),
timestamp = System.DateTime.UtcNow.ToString("o")
};
var json = JsonUtility.ToJson(report);
File.WriteAllText(
Path.Combine(reportPath, "report.json"), json);
// Save screenshot separately
if (screenshot != null)
File.WriteAllBytes(
Path.Combine(reportPath, "screenshot.png"), screenshot);
}
public IEnumerator FlushQueue()
{
if (!Directory.Exists(QueuePath)) yield break;
var dirs = Directory.GetDirectories(QueuePath);
foreach (var dir in dirs)
{
var jsonPath = Path.Combine(dir, "report.json");
if (!File.Exists(jsonPath)) continue;
var report = JsonUtility.FromJson<QueuedReport>(
File.ReadAllText(jsonPath));
var screenshotPath = Path.Combine(dir, "screenshot.png");
byte[] screenshot = File.Exists(screenshotPath)
? File.ReadAllBytes(screenshotPath) : null;
bool sent = false;
yield return Submit(report.title, report.description,
report.category, screenshot, (ok) => sent = ok);
if (sent)
Directory.Delete(dir, true);
}
}
}
Call FlushQueue on game startup and periodically during gameplay (every five minutes is a reasonable interval). Only delete queued reports after the server confirms receipt. If the flush fails, the reports stay on disk and will be retried on the next attempt.
Wiring It All Together
The submit handler connects the UI to the reporter:
private void OnSubmit()
{
if (string.IsNullOrWhiteSpace(_titleField.text))
{
// Show validation error
return;
}
_submitButton.interactable = false;
var category = _categoryDropdown.options[
_categoryDropdown.value].text;
StartCoroutine(_reporter.Submit(
_titleField.text,
_descriptionField.text,
category,
_screenshotData,
(success) =>
{
_panel.SetActive(false);
Time.timeScale = 1f;
_submitButton.interactable = true;
// Show thank-you toast
}
));
}
Add a keyboard shortcut like F8 to open the reporter. Make sure it works even when the game is in a broken state — if the player is stuck in a softlock, they need the reporter most. Test the reporter by triggering it during loading screens, cutscenes, paused states, and in menus to ensure it works everywhere.
For shipping to production, replace the hardcoded API key with one loaded from a config file or ScriptableObject. Never include admin keys or secrets in client builds — use a project-level submission key that only has permission to create new bug reports.
“The best bug report is the one that gets submitted. Every field you add to your form, every extra click you require, reduces the number of players who will finish reporting.”
Related Issues
For the Godot equivalent of this guide, see how to add an in-game bug reporter to your Godot game. If you want to add automatic crash reporting alongside manual bug reports, check how to add crash reporting in 10 minutes. For building a public-facing tracker where these reports land, see how to create a public bug tracker for your game.
Ship the bug reporter from day one of early access. The reports you collect in the first week of real player testing are worth more than a month of internal QA.