🤑 Indie game store🙌 Free games😂 Fun games😨 Horror games
👷 Game development🎨 Assets📚 Comics
🎉 Sales🎁 Bundles

[Unity Asset] Easy Feedback Form: In-game bug reporting for Unity

A topic by Noah Ratcliff created 280 days ago Views: 987 Replies: 9
Viewing posts 1 to 9
(Edited 12 times) (+1)

Get Easy Feedback now!

Current Version 1.0.5

What is Easy Feedback?

Easy Feedback streamlines bug reporting and player feedback in Unity games by bringing it directly to developers.

Detailed feedback and bug reports are sent from in-game to Trello, where these they are easily accessible, read, and organized.

How does it work?

When a player submits a report, it is categorized and directed to a list in the developer's Trello board based on the type of feedback. Included along with the report is a screenshot taken the instant the Feedback Form is opened, system information (OS, CPU, GPU, etc.), quality settings (resolution, quality level, etc), and a log of all messages, warnings and exceptions since the last report.

All of this information is formatted with markdown on Trello for easy reading.

Current features

  • Bug reports and feedback sent directly to Trello
  • One-click Trello setup without leaving the Unity editor
  • Reports automatically categorized and organized
  • Highly customizable UI
  • Categories in form derived from lists on Trello
  • Tagging by bug priority level
  • Detailed reports including:
    • Screenshot
    • Debug log and exceptions
    • Stack trace
    • System information (OS, CPU, GPU, etc.)
    • Unity player information (resolution, quality level, etc.)
  • API for adding custom game-specific information to the report
  • Ability to add multiple file attachments to the report
  • Callbacks for form opened, closed, and submitted
  • Modular system allowing custom fields to be added to the form
  • Customizable feedback key and message
  • Store reports locally (Expo Mode)

Easy Feedback in action

I've been working closely with Twitch streamer QaziTV to get feedback and live testing of the asset while he develops his latest game. Here are some screenshots from his game, and his feedback board in Trello:

In-game

Easy Feedback in-game

Example report

Example report

In Trello

Trello board

Easy Feedback in Unity

I've put a considerable amount of time into trying to streamline authentication and setup with the Trello API in Unity. From the beginning, the goal was to be able to authenticate, create a new board, or select an existing one for feedback all in the editor. Eventually, I'd also like developers to be able to customize their boards (adding categories and tags) from within the editor, but this is not currently supported.

Easy Feedback in inspector when first added to the scene

Easy Feedback in inspector

Trello authentication

Trello authentication

After authentication

After authentication

Creating a new feedback board

Creating a new feedback board

Selecting a feedback board

Selecting a feedback board

Thanks for reading! I'd love to hear your questions, thoughts, and feedback!

This looks like a great tool!

Will it be open source? How easy would it be to integrate it with something other than Trello? (Airtable in my case, but i'm happy to hardcode API access for my purpose)

Thanks!

Right now, I am focusing on Trello, but it's definitely a goal to make things modular enough that users can replace the Trello API with a service of their choosing. When you purchase the asset you will have access, and rights, to modify the source as you see fit, so long as you don't redistribute that code.

Airtable looks really interesting! I'll have to look into it for the future :)

Thanks for the reply! I'll keep an eye on this :)

Including custom information in your feedback

This next couple weeks, I'm focusing on making Easy Feedback more modular so developers can write scripts adding their own functionality to the feedback form. Here's what I'm currently working on:

  • UnityEvents for registering callback functions when the form is opened, submitted, and closed.
  • Ability to append/modify information sent with the report
  • Modular system for adding fields to the feedback form

With the latest update, I've crossed off the second bullet on that list by exposing the report data publicly from the feedback form script. With the aid of some helper methods, developers can modify existing information sent with the report, and add their own sections to the report.

The feature is currently a bit rough, but the goal is to expose a simple barebones API, and add features as use cases crop up in early access.

OnFormOpened, OnFormSubmitted, and OnFormClosed callbacks

Until now, developers have just been modifying the FeedbackForm script directly to add their custom feedback code. This was pretty unsustainable, as every time they updated the asset, it'd break their changes! This week's update addresses that, adding 3 UnityEvents to the FeedbackForm script: OnFormOpened, OnFormSubmitted, and OnFormClosed.

Honestly, I should have added these sooner, as I've gotten some reports from developers that they had not been updating the asset with weekly builds because they didn't want their form to break, and to have to rewrite it. Hopefully this will help mitigate the fear of updating for the future.

Here's exactly when each event is invoked:

OnFormOpened: This event is invoked right after the screenshot is taken, and right before the form appears on the screen. Any information that must be collected when the user begins to submit a report should be collected here.

OnFormSubmitted: This event is invoked right before the report is sent to Trello, after the user clicks the "submit" button. This would be a good time to add information from custom fields on the form to the report.

OnFormClosed: This event is called whenever the form closes, whether a report was submitted or not. This is a good time to reset any custom fields on the form, or clean up any temporary resources created for the report (this is where the screenshot taken with the form is deleted!).

These core three events will not be changing much during the rest of the alpha, so developers rest assured that you can hook in your scripts without the next update breaking your form.

GDC 2017

Next week I'll be attending the Game Developers Conference, so there will not be an update for Easy Feedback. However, I will be writing up a more technical devlog post on how the WebView for Trello authentication works, so keep your eye out for that! If you are attending GDC and would like to meet up and chat, reach out to me on Twitter! I'll be handing out Easy Feedback discount keys, so be sure to nab one!

(Edited 2 times)

Consuming Unity's WebView API

or how I ended up with a one-way ticket on the Unity pain train

In lieu of an update this week, I'd like to talk in depth about one of the more technically challenging parts of Easy Feedback: in-editor authentication via WebView.

Before launching early access, Easy Feedback was in somewhat of a "closed beta" state. I had given the asset to fellow game developer and streamer, Qazi, to use in his latest game, Shotgun Farmers. At that time, the only way to authenticate with Trello was to navigate to the API homepage in a browser, and request an application key and secret. Qazi then had to edit a script that held the Trello API key and secret as constant strings (aptly named TrelloAPIConfig), adding his key and secret. This was all a holdover from when the feedback form was originally written for my game, Noah and The Quest to Turn on The Light. At the time, I hadn't planned to release the form as a standalone asset, so I didn't put much thought into reuse.

Before launching the public alpha, one of the first priorities was abstracting users away from the Trello API as much as possible. Ideally, I wanted to keep the users within the editor at all times. With a constant application key for all of Easy Feedback, I can direct users to a URL where they will be required to log in and give Easy Feedback permission to access their account. After they give permission, users are given a single token that they can copy and paste into the Easy Feedback script. Easy enough, now we just need to show all that in the editor.

This is where things get tricky. The WebView API (as seen in action in the Asset Store window), is not publicly accessible. In fact, it's an internal sealed class, and there's no documentation on it at all. But, I was determined. If the Asset Store could display web content, I sure could too. Accessing non-public types, methods, and fields in C# isn't impossible, but it's also not easy.

Thanks to reflection, if I expect a type, method, or field to exist, I can find it, execute it, or access and modify it, whether public, private, sealed, or internal. The downside to this technique is that I have to know the exact definition of the type ahead of time, and any mistakes result in frustratingly vague runtime exceptions. This is where Matt Rix's Unity Decompiled repository came in very handy. Not could I reference the full definition of the WebView class, but I could also reference the AssetStoreWindow class to figure out how to properly use WebView.

With all that covered, let's talk about how the authentication window implementation works.

Initializing the EditorWindow

When the window is first initialized, we have to do a few things:

First, we get the HostView for the window. This is a private field in EditorWindow, named "m_Parent." We'll need this later, when initializing the WebView.

FieldInfo parentInfo = typeof(WebWindow).GetField("m_Parent", ALL_FLAGS);
guiViewParent = parentInfo.GetValue(this);

Next, we search the application assemblies for the WebView and GUIClip types. Finding the types is pretty simple, we just search through each assembly in the current application domain until we find a type that matches a given string name, then return that type.

private Type getTypeFromAssemblies(string typeName)
{
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    foreach (Assembly assembly in assemblies)
    {
        Type[] types = assembly.GetTypes();
        foreach (Type type in types)
        {
            if (type.Name.Equals(typeName, StringComparison.CurrentCultureIgnoreCase)
                || type.Name.Contains('+' + typeName))
                return type;
        }
    }
    Debug.LogWarning("Could not find type \"" + typeName + "\" in assemblies.");
    return null;
}

We only need the static "Unclip" function from GUIClip, so we go ahead and reflect out the method information from the type. We pass in the Rect type when calling GetMethod on the type, as GUIClip contains multiple definitions for Unclip, and we want the one that returns a Rect.

// get GUIClip.Unclip
unclipMethod = guiClipType.GetMethod("Unclip", new[] { typeof(Rect) });

Initializing the WebView

Disclaimer: because of the lack of documentation, all the code from here on out is the result of heavy reference of AssetStoreWindow, and a lot of trial and error. I make no promises that this is the correct, most efficient, or even most stable way to render a WebView. By continuing, the reader assumes all liability for all emotional and physical damage as a result of reading past this point.

private void initWebView()
{
    // dispose and destroy webView
    if ((ScriptableObject)webView)
    {
        DestroyImmediate((ScriptableObject)webView);
        webView = null;
    }
    // create the webview
    webView = ScriptableObject.CreateInstance(webViewType);
    webViewType.GetMethod("InitWebView").Invoke(webView,
        new object[] { guiViewParent, (int)position.x, (int)position.y, (int)position.width, (int)position.height, false });
    // load url
    webViewType.GetMethod("SetDelegateObject").Invoke(webView, new object[] { this });
    loadURLMethod = webViewType.GetMethod("LoadURL");
    loadURLMethod.Invoke(webView, new object[] { URL });
    // get methods from webview
    showMethod = webViewType.GetMethod("Show");
    setFocusMethod = webViewType.GetMethod("SetFocus");
    setSizeAndPositionMethod = webViewType.GetMethod("SetSizeAndPosition");
    reloadMethod = webViewType.GetMethod("Reload");
    executeJSMethod = webViewType.GetMethod("ExecuteJavascript");
}

First, we check if we already have a reference to an existing WebView object. If so, we destroy it so that we can initialize a new one. Next, we instantiate the WebView with ScriptableObject.CreateInstance, and invoke the InitWebView function on our newly created object. This is a good time to talk about why Unity Decompiled is so helpful: When using reflection to invoke a method on an object, you need to know the method's full signature. All parameters are passed in as an array of objects that must all be present, of the correct type and in the correct order, or the runtime gods will smite you with incredibly unhelpful exceptions or editor crashes. Referencing Unity Decompiled is the only tool we have to fight the runtime gods. This is where we pass in the GUIView we retrieved in init.

// create the webview
webView = ScriptableObject.CreateInstance(webViewType);
webViewType.GetMethod("InitWebView").Invoke(webView,
    new object[] { guiViewParent, (int)position.x, (int)position.y, (int)position.width, (int)position.height, false });

Next, we invoke the SetDelegateObject method on the WebView, and pass in the current EditorWindow instance. Then, we reflect out the LoadURL method, invoke it with the current URL as the only parameter, and save the method info for later use.

// load url
webViewType.GetMethod("SetDelegateObject").Invoke(webView, new object[] { this });
loadURLMethod = webViewType.GetMethod("LoadURL");
loadURLMethod.Invoke(webView, new object[] { URL });

Finally, we reflect some helpful methods out of WebView, and save them for future use. For each of the methods, we'll also write helpful wrapper methods for quick and clean invocation.

// get methods from webview
showMethod = webViewType.GetMethod("Show");
setFocusMethod = webViewType.GetMethod("SetFocus");
setSizeAndPositionMethod = webViewType.GetMethod("SetSizeAndPosition");
reloadMethod = webViewType.GetMethod("Reload");
executeJSMethod = webViewType.GetMethod("ExecuteJavascript");

Rendering the WebView

If you thought we were flying pretty loose and wild before, strap in.

private void OnGUI()
{
    if (unclipMethod != null)
    {
        webViewRect = (Rect)unclipMethod.Invoke(null, new object[]
        {
            new Rect(0f, 0f, base.position.width, base.position.height)
        });
    }
    else
    {
        init();
    }
    //Rect webViewRect = new Rect(0f, 0f, base.position.width, base.position.height);
    if (webView == null)
    {
        init();
    }
    else if (Event.current.type == EventType.Repaint)
    {
        try
        {
            //doGUIMethod.Invoke(webView, new object[] { webViewRect });
            setSizeAndPositionMethod.Invoke(webView, new object[]
            {
                (int)webViewRect.x,
                (int)webViewRect.y,
                (int)webViewRect.width,
                (int)webViewRect.height
            });
            // set white background
            executeJS("if(document.getElementsByTagName('body')[0].style.backgroundColor === '') { document.getElementsByTagName('body')[0].style.backgroundColor = 'white'; }");
        }
        catch (TargetInvocationException e)
        {
            // web view was disposed, but not by us :(
            webView = null;
            DestroyImmediate(this);
        }
    }
}

I'd like to address a reoccurring trend in this block. Sometimes, OnGUI may be called before init, or references may be cleaned up by some mysterious outside force. To counter this, we take care to check that things are not null before trying to use them. If they are we just initialize the window again.

The first thing we do in OnGUI is invoke the Unclip method we retrieved in init. To be completely honest, I'm not completely sure what this does. But the Asset Store window does it, and it works.

if (unclipMethod != null)
{
    webViewRect = (Rect)unclipMethod.Invoke(null, new object[]
    {
        new Rect(0f, 0f, base.position.width, base.position.height)
    });
}
else
{
    init();
}

If this call to OnGUI is the repaint pass, we attempt to Invoke SetSizeAndPosition on the WebView, setting it to the size and position of the current editor window. Sometimes, our WebView object may still end up null here. In that case, we just go ahead and kill the window. This was more of an issue in development, and I haven't been able to reproduce it, but I'd rather be safe than sorry.

if (webView == null)
{
    init();
}
else if (Event.current.type == EventType.Repaint)
{
    try
    {
        //doGUIMethod.Invoke(webView, new object[] { webViewRect });
        setSizeAndPositionMethod.Invoke(webView, new object[]
        {
            (int)webViewRect.x,
            (int)webViewRect.y,
            (int)webViewRect.width,
            (int)webViewRect.height
        });
        // set white background
        executeJS("if(document.getElementsByTagName('body')[0].style.backgroundColor === '') { document.getElementsByTagName('body')[0].style.backgroundColor = 'white'; }");
    }
    catch (TargetInvocationException e)
    {
        // web view was disposed, but not by us :(
        webView = null;
        DestroyImmediate(this);
    }
}

An extra fun feature of the WebView is that any webpage without a defined background color will render with a transparent background. This isn't an issue of course, because the Trello API pages all define a background color, right?

There's no background color.

Heck.

From what I can tell, the WebView mostly takes over and handles rendering within its delegate editor window on its own, and simply rendering a blank white texture in the background doesn't work. Great.

But, it's 2017, and no web problem is solved in 2017 without JavaScript. Luckily for us, WebView has an ExecuteJS method lying around. We call our wrapper function, and execute JavaScript that sets the page's background as white if it does not already have a defined background color. We do this on every OnGUI call.

Excuse me, I need a moment.

// set white background
executeJS("if(document.getElementsByTagName('body')[0].style.backgroundColor === '') { document.getElementsByTagName('body')[0].style.backgroundColor = 'white'; }");

OnFocus and OnLostFocus

Whenever the window gains or loses focus, we invoke the SetFocus method on the WebView, and pass in the focus state as a bool. If the window is currently being focused, we also invoke SetHostView on the WebView and pass in the editor window's GUIView, then invoke the Show method on the WebView. Again, the Asset Store does this, so we're doing it.

private void OnFocus()
{
    setFocus(true);
}
private void OnLostFocus()
{
    setFocus(false);
}
private void setFocus(bool focus)
{
    if (webView != null)
    {
        try
        {
            if (focus)
            {
                webViewType.GetMethod("SetHostView").Invoke(webView, new object[] { guiViewParent });
                showMethod.Invoke(webView, null);
            }
            setFocusMethod.Invoke(webView, new object[] { focus });
        }
        catch (TargetInvocationException e)
        {
            // web view was disposed before we expected it :(
            webView = null;
            DestroyImmediate(this);
        }
    }
}

OnDestroy

Don't worry, this is all almost over. Finally, when the editor window is destroyed, we invoke DestroyWebView on the WebWindow.

private void OnDestroy()
{
    // destroy web view
    if (webView != null)
    {
        webViewType.GetMethod("DestroyWebView", ALL_FLAGS).Invoke(webView, null);
        webView = null;
    }
}

Final thoughts

If you enjoy the Unity Editor crashing every time your scripts recompile being a primary element of your debugging workflow, I highly recommend trying out implementing an undocumented internal API.

There's a lot of mess and inefficiency in this script, but it's functional for the time being, and the end result really does improve the authentication flow. When I have more core features implemented, I'll be coming back and cleaning things up before full release.

(Edited 1 time)

Custom feedback categories

Last week, Easy Feedback 0.7.0 was released to early access developers. With this release, developers can now define custom feedback categories, derived from lists on their Trello board.

Previously, reports were categorized into two predefined categories, "Feedback" and "Bugs." All feedback boards required respective lists for both categories, and the lists could not be removed or renamed.

However, users can now define any number of categories with any name they wish. Any list with the text "(EF)" at the end of its name is a valid feedback category, and will added to the categories dropdown on the form. The name of the category on the form is derived from the name of its respective list.

Custom categories in action

Here I have a new board on Trello. I have created two feedback categories named, "My Category" and "My Other Category." I've also added a third non-category list.

When I load the new board from my Trello account in the editor, the categories are found on the board, and added to the categories dropdown on the form.

In-game, I can select my categories from the dropdown when submitting a report. Let's go ahead and label this report as "My Category."

After I submit my report, it appears under the "My Category" list!

Notes

If any new categories are added to the board, or categories are renamed on Trello, those changes will not be reflected until the board is refreshed in the editor, and a new build is made. This behaviour is intentional, but subject to change before release.

Currently, the only way to add, remove, or rename categories is by editing the lists on Trello. However, a nice UI for managing categories in-editor is planned for an upcoming update to the alpha.

(Edited 1 time)

Last update to Early Access before release!

Easy Feedback has come a long way since the project started late last year, mostly thanks to the support of the pioneering Early Access developers who supported development not only by paying for an incomplete product, but in their amazing feedback and bug reports (many of which were often fixed by the developer before I could even reply to the post!). I really appreciate the overwhelming support during development.

With that, I'm happy to announce that the latest update, 1.0.0b1, is the last major update to Easy Feedback before official release on itch.io and the Unity Asset Store.

After getting the last round of bug reports from Early Access developers, I'll be sending the asset off for approval and listing on the Asset Store. Right now, we're looking at an early May release.

There's been lots of awesome features added to Easy Feedback since the last devlog update, and I'll be doing a series of longer, more focused, writeups for each of them in the weeks leading up to release.

If you'd like information on the release as we get closer to May, consider signing up for the Easy Feedback mailing list!

Easy Feedback is now available on itch and the Unity Asset Store!

Easy Feedback has now been officially released on itch and the Unity Asset Store!

A huge thanks to all of the early access developers who gave incredible and invaluable feedback during the development process. Easy Feedback wouldn't be as great as it is today without them.