Digital Software Publishing Stockholm

Home › Unity tools › Guides

Unusable playtest bug reports? Capture them inside your Unity game

What a usable playtest bug report contains, where each piece comes from in Unity 6, and how to capture and send it from inside the game, with C# snippets.

2026-10-29 · Digital Software · about Bug Capsule

"It crashed when I jumped" is not a report

A playtester writes "it crashed when I jumped" in Discord, three hours after it happened, from a build you replaced yesterday. You cannot reproduce it, you do not know which scene they were in, and the one thing that would have told you the cause, the exception and its stack trace, sits in a Player.log file they have never heard of.

Testers are playing, not filing tickets. If you want usable reports, the game has to collect the facts itself, at the moment the tester presses a key.

The minimum set

A report you can act on holds these pieces:

Everything except the player's words can be captured automatically.

Where each piece comes from in Unity

Build and system

Application.version returns the version string from Edit > Project Settings > Player. Application.unityVersion is the runtime version. Application.buildGUID is unique per build and empty in the editor, a cheap way to tell builds apart. For the commit hash, have your build script write a small JSON file into StreamingAssets and read it at startup; Unity has no built-in field for it.

The SystemInfo class covers the hardware side:

string system =
    "os=" + SystemInfo.operatingSystem +
    "\ndevice=" + SystemInfo.deviceModel +
    "\ncpu=" + SystemInfo.processorType +
    "\nram_mb=" + SystemInfo.systemMemorySize +
    "\ngpu=" + SystemInfo.graphicsDeviceName + " / " + SystemInfo.graphicsDeviceVersion +
    "\nvram_mb=" + SystemInfo.graphicsMemorySize +
    "\nunity=" + Application.unityVersion +
    "\nversion=" + Application.version +
    "\nbuild=" + Application.buildGUID;

Add SceneManager.GetActiveScene().name and Time.realtimeSinceStartup for the scene and the play time.

The log, kept from game start

The Player log is on disk, but asking testers to find it is where most reports die. The locations differ per platform: on Windows it is %USERPROFILE%\AppData\LocalLow\CompanyName\ProductName\Player.log, on macOS ~/Library/Logs/Company Name/Product Name/Player.log, on Linux ~/.config/unity3d/CompanyName/ProductName/Player.log. Android logs go to logcat, iOS to the Xcode console, and Web builds to the browser's JavaScript console. There is no file to attach on three of those platforms.

So keep your own copy in memory. Application.logMessageReceivedThreaded is raised for every message, from any thread, so the handler must be thread-safe. You do not need logMessageReceived as well; the threaded variant also fires for main-thread messages.

using System.Collections.Generic;
using UnityEngine;

public class LogRing : MonoBehaviour
{
    const int Capacity = 500;
    static readonly Queue<string> lines = new Queue<string>();
    static readonly object gate = new object();

    void OnEnable() { Application.logMessageReceivedThreaded += OnLog; }
    void OnDisable() { Application.logMessageReceivedThreaded -= OnLog; }

    static void OnLog(string message, string stackTrace, LogType type)
    {
        string line = type + ": " + message;
        if (type == LogType.Error || type == LogType.Exception)
            line += "\n" + stackTrace;
        lock (gate)
        {
            lines.Enqueue(line);
            if (lines.Count > Capacity) lines.Dequeue();
        }
    }

    public static string Dump()
    {
        lock (gate) return string.Join("\n", lines);
    }
}

Put this component in your first scene so the ring starts filling at launch. A ring capped by count costs almost nothing and keeps the message from before the moment.

The screenshot

Take the screenshot before the panel shows, or the panel will be in it. ScreenCapture.CaptureScreenshotAsTexture must run after the frame has finished rendering; the documented way is a coroutine that yields on WaitForEndOfFrame.

IEnumerator CaptureThenOpen()
{
    yield return new WaitForEndOfFrame();
    Texture2D shot = ScreenCapture.CaptureScreenshotAsTexture();
    byte[] png = shot.EncodeToPNG();
    Destroy(shot);
    OpenPanel(png);
}

Start the coroutine when the report key is pressed, and destroy the texture afterwards.

Recent inputs

With the Input System package, InputEventTrace records input events into a fixed buffer. When the buffer is full, old events are overwritten, so it behaves as a ring. Size it in bytes, not seconds; mouse motion produces many events per second, so a 60 second window with pointer motion needs a bigger buffer.

using UnityEngine.InputSystem.LowLevel;

InputEventTrace trace = new InputEventTrace(bufferSizeInBytes: 256 * 1024);
trace.Enable();

// at report time
trace.WriteTo(System.IO.Path.Combine(reportFolder, "inputs.inputtrace"));

// on quit
trace.Dispose();

The trace holds unmanaged memory; dispose it or it leaks.

Game state when the panel opens

Opening a panel mid-game touches state the game owns. Save it, change it, and restore it exactly:

CursorLockMode lockState = Cursor.lockState;
bool cursorVisible = Cursor.visible;
float timeScale = Time.timeScale;

Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
Time.timeScale = 0f;

// when the panel closes
Cursor.lockState = lockState;
Cursor.visible = cursorVisible;
Time.timeScale = timeScale;

Two more things bite. A game that drives input itself may have no EventSystem in the scene, and without one uGUI buttons do nothing; add one while the panel is up and remove it afterwards. Gameplay actions also keep firing while the tester types, so disable that action map for the duration.

Where reports go

A folder under Application.persistentDataPath is the simplest sink: no network, no secrets, works offline, and the tester zips the folder and sends it to you. On Windows that path is under %userprofile%\AppData\LocalLow\<companyname>\<productname>.

A Discord webhook is the usual sink for small teams. Execute Webhook takes multipart/form-data with the JSON in a payload_json field and files in files[0], files[1] and so on. The content field holds up to 2000 characters, up to 10 embeds are allowed, and the default upload limit is 20 MiB per file.

IEnumerator PostToDiscord(string webhookUrl, string text, byte[] png, byte[] zip)
{
    string payload = "{\"content\":" + JsonText(text)
        + ",\"allowed_mentions\":{\"parse\":[]}}";
    var form = new List<IMultipartFormSection>
    {
        new MultipartFormDataSection("payload_json", payload),
        new MultipartFormFileSection("files[0]", png, "screenshot.png", "image/png"),
        new MultipartFormFileSection("files[1]", zip, "report.zip", "application/zip")
    };
    using UnityWebRequest req = UnityWebRequest.Post(webhookUrl, form);
    yield return req.SendWebRequest();
    if (req.result != UnityWebRequest.Result.Success) Debug.LogError(req.error);
}

static string JsonText(string s) =>
    "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"")
            .Replace("\n", "\\n").Replace("\r", "\\r") + "\"";

Your own HTTP endpoint takes the same multipart POST; you set the limits and the relay. A GitHub issue is text only; the API cannot attach files, so the issue should say where the files went.

Two security points

Never ship a GitHub token in a public build. Strings in a build can be extracted, and a token lets anyone do whatever it permits in your repository. Use GitHub only in closed-playtest builds, with a fine-grained token limited to one repository and issue write access, and revoke it when the playtest ends. A leaked Discord webhook URL is less serious: the worst case is spam in one channel, and deleting the webhook ends it.

Sanitize player text before it reaches Discord. allowed_mentions with "parse": [] stops the message from pinging anyone, and replacing @ in the text covers the case where you forget it. Crop the text to the 2000 character limit, say so, and keep the full text in the attached file.

Where Bug Capsule fits

If you would rather not maintain the above, Bug Capsule is this publisher's runtime package that does it: 29.99 USD, Unity 6.3 LTS and newer, in review on the Asset Store as of 2026-10-29. The tester presses F8 or taps with three fingers, draws on the screenshot with rectangles, arrows, a pen and numbered pins, writes a title and presses Send. The report carries the log with stack traces kept from game start, a 60 second Input System trace, a system snapshot, the scene and play time, your custom fields and attachments, and the build stamp when the build has one.

Sinks are a folder, Discord, your webhook and a GitHub issue, sent in the background with per-sink progress. Discord text is cleaned and packages are kept under 10 MB. Public and Tester token modes come with a build guard that fails a build holding a GitHub token in Public mode. Cursor, time scale and EventSystem are restored exactly.

Limits in 1.0.0: the panel is landscape only, 16:9 or wider; macOS and iOS are not tested; there is no video clip.

Sources

More guides

All guides · Atom feed