Skip to content

Commit 2bfbb57

Browse files
authored
feat!: pass result to callback (#91)
* feat!: pass result to callback BREAKING CHANGE: changes public interface for Post callbacks * fix: build error for IExceptionReporter implementation * fix: sharing violation attaching editor and user logs * fix: don't skip posting first report * fix: don't attempt to post with destroyed game object (#92) * fix: don't attempt to post with destroyed game object * fix: windows build * fix: updated sample * feat: add support response to sample * fix: open web browser on linux * fix: invoke callback in webgl * fix: remove support response from webgl * chore: update readme * chore: update README.md
1 parent b7ae597 commit 2bfbb57

26 files changed

+721
-431
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,69 @@ bugsplat.ShouldPostException = (ex) =>
196196
};
197197
```
198198

199+
### Support Response
200+
201+
BugSplat has the ability to display a support response to users who encounter a crash. You can show your users a generalized support response for all crashes, or a custom support response that corresponds to the type of crash that occurred. Defining a support response allows you to alert users that bug has been fixed in a new version, or that they need to update their graphics drivers.
202+
203+
Next, pass a callback to `bugsplat.Post`. In the callback handler add code to open the support response in the user's browser. A full example can be seen in [ErrorGenerator.cs](https://github.com/BugSplat-Git/bugsplat-unity/blob/main/Samples~/my-unity-crasher/Scripts/ErrorGenerator.cs).
204+
205+
```cs
206+
private string infoUrl = "";
207+
208+
public void Event_CatchExceptionThenPostNewBugSplat()
209+
{
210+
try
211+
{
212+
GenerateSampleStackFramesAndThrow();
213+
}
214+
catch (Exception ex)
215+
{
216+
var options = new ReportPostOptions()
217+
{
218+
Description = "a new description"
219+
};
220+
221+
StartCoroutine(bugsplat.Post(ex, options, ExceptionCallback));
222+
}
223+
}
224+
225+
void ExceptionCallback(ExceptionReporterPostResult result)
226+
{
227+
UnityEngine.Debug.Log($"Exception post callback result: {result.Message}");
228+
229+
if (result.Response == null) {
230+
return;
231+
}
232+
233+
UnityEngine.Debug.Log($"BugSplat Status: {result.Response.status}");
234+
UnityEngine.Debug.Log($"BugSplat Crash ID: {result.Response.crashId}");
235+
UnityEngine.Debug.Log($"BugSplat Support URL: {result.Response.infoUrl}");
236+
237+
infoUrl = result.Response.infoUrl;
238+
}
239+
240+
private void OpenUrl(string url)
241+
{
242+
var escaped = url.Replace("?", "\\?").Replace("&", "\\&").Replace(" ", "%20").Replace("!", "\\!");
243+
244+
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_WSA
245+
Process.Start(url);
246+
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
247+
Process.Start("open", escaped);
248+
#elif UNITY_STANDALONE_LINUX || UNITY_EDITOR_LINUX
249+
Process.Start("xdg-open", escaped);
250+
#else
251+
UnityEngine.Debug.Log($"OpenUrl unsupported platform: {Application.platform}");
252+
#endif
253+
}
254+
```
255+
256+
When an exception occurs, a page similar to the following will open in the user's browser on Windows, macOS, and Linux.
257+
258+
<img width="1086" alt="image" src="https://github.com/user-attachments/assets/3a3d6f82-e3bf-42bc-ae7f-582ba35cd499">
259+
260+
More information on support responses can be found [here](https://docs.bugsplat.com/introduction/production/setting-up-custom-support-responses).
261+
199262
## 🪟 Windows
200263

201264
BugSplat can be configured to upload Windows minidumps created by the `UnityCrashHandler`. BugSplat will automatically pull Unity Player symbols from the [Unity Symbol Server](https://docs.unity3d.com/Manual/WindowsDebugging.html). If your game contains Native Windows C++ plugins, `.dll` and `.pdb` files in the `Assets/Plugins/x86` and `Assets/Plugins/x86_64` folders will be uploaded by BugSplat's PostBuild script and used in symbolication.

Runtime/BugSplat.cs

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Collections;
77
using System.Collections.Generic;
88
using System.IO;
9-
using System.Linq;
109
using System.Net.Http;
1110
using System.Runtime.CompilerServices;
1211
#if UNITY_IOS && !UNITY_EDITOR
@@ -201,28 +200,23 @@ bool useNativeLibAndroid
201200
throw new ArgumentException("BugSplat error: version cannot be null or empty");
202201
}
203202

204-
var gameObject = new GameObject();
205-
206-
#if UNITY_STANDALONE_WIN || UNITY_WSA
207-
var bugsplat = new BugSplatDotNetStandard.BugSplat(database, application, version);
208-
bugsplat.MinidumpType = BugSplatDotNetStandard.BugSplat.MinidumpTypeId.UnityNativeWindows;
209-
bugsplat.ExceptionType = BugSplatDotNetStandard.BugSplat.ExceptionTypeId.Unity;
203+
#if UNITY_STANDALONE_WIN || UNITY_WSA
204+
var bugsplat = new BugSplatDotNetStandard.BugSplat(database, application, version)
205+
{
206+
MinidumpType = BugSplatDotNetStandard.BugSplat.MinidumpTypeId.UnityNativeWindows,
207+
ExceptionType = BugSplatDotNetStandard.BugSplat.ExceptionTypeId.Unity
208+
};
210209
var dotNetStandardClientSettings = new DotNetStandardClientSettingsRepository(bugsplat);
211210
var dotNetStandardClient = new DotNetStandardClient(bugsplat);
212-
var dotNetStandardExceptionReporter = DotNetStandardExceptionReporter.Create(dotNetStandardClientSettings, dotNetStandardClient, gameObject);
211+
var dotNetStandardExceptionReporter = new DotNetStandardExceptionReporter(dotNetStandardClientSettings, dotNetStandardClient);
213212
var windowsReporter = new WindowsReporter(dotNetStandardClientSettings, dotNetStandardExceptionReporter, dotNetStandardClient);
214-
215213
clientSettings = dotNetStandardClientSettings;
216214
exceptionReporter = windowsReporter;
217215
nativeCrashReporter = windowsReporter;
218216
#elif UNITY_WEBGL
219217
var webGLClientSettings = new WebGLClientSettingsRepository();
220218
var webGLExceptionClient = new WebGLExceptionClient(database, application, version);
221-
var webGLReporter = WebGLReporter.Create(
222-
webGLClientSettings,
223-
webGLExceptionClient,
224-
gameObject
225-
);
219+
var webGLReporter = new WebGLReporter(webGLClientSettings, webGLExceptionClient);
226220
clientSettings = webGLClientSettings;
227221
exceptionReporter = webGLReporter;
228222
#elif UNITY_IOS && !UNITY_EDITOR
@@ -241,13 +235,13 @@ bool useNativeLibAndroid
241235
javaClass.CallStatic("initBugSplat", activity, database, application, version);
242236
}
243237

244-
UseDotNetHandler(database, application, version, gameObject);
238+
UseDotNetHandler(database, application, version);
245239
#else
246-
UseDotNetHandler(database, application, version, gameObject);
240+
UseDotNetHandler(database, application, version);
247241
#endif
248242
}
249243

250-
private void UseDotNetHandler(string database, string application, string version, GameObject gameObject)
244+
private void UseDotNetHandler(string database, string application, string version)
251245
{
252246
var bugsplat = new BugSplatDotNetStandard.BugSplat(database, application, version)
253247
{
@@ -256,7 +250,7 @@ private void UseDotNetHandler(string database, string application, string versio
256250
};
257251
var dotNetStandardClientSettings = new DotNetStandardClientSettingsRepository(bugsplat);
258252
var dotNetStandardClient = new DotNetStandardClient(bugsplat);
259-
var dotNetStandardExceptionReporter = DotNetStandardExceptionReporter.Create(dotNetStandardClientSettings, dotNetStandardClient, gameObject);
253+
var dotNetStandardExceptionReporter = new DotNetStandardExceptionReporter(dotNetStandardClientSettings, dotNetStandardClient);
260254

261255
clientSettings = dotNetStandardClientSettings;
262256
exceptionReporter = dotNetStandardExceptionReporter;
@@ -270,24 +264,27 @@ public static BugSplat CreateFromOptions(BugSplatOptions options)
270264
{
271265
var application = string.IsNullOrEmpty(options.Application) ? Application.productName : options.Application;
272266
var version = string.IsNullOrEmpty(options.Version) ? Application.version : options.Version;
273-
267+
268+
274269
var bugSplat = new BugSplat(
275270
options.Database,
276271
application,
277-
version,
272+
version,
273+
278274
options.UseNativeCrashReportingForIos,
279275
options.UseNativeCrashReportingForAndroid
280-
);
281-
282-
bugSplat.Description = options.Description;
283-
bugSplat.Email = options.Email;
284-
bugSplat.Key = options.Key;
285-
bugSplat.Notes = options.Notes;
286-
bugSplat.User = options.User;
287-
bugSplat.CaptureEditorLog = options.CaptureEditorLog;
288-
bugSplat.CapturePlayerLog = options.CapturePlayerLog;
289-
bugSplat.CaptureScreenshots = options.CaptureScreenshots;
290-
bugSplat.PostExceptionsInEditor = options.PostExceptionsInEditor;
276+
)
277+
{
278+
Description = options.Description,
279+
Email = options.Email,
280+
Key = options.Key,
281+
Notes = options.Notes,
282+
User = options.User,
283+
CaptureEditorLog = options.CaptureEditorLog,
284+
CapturePlayerLog = options.CapturePlayerLog,
285+
CaptureScreenshots = options.CaptureScreenshots,
286+
PostExceptionsInEditor = options.PostExceptionsInEditor
287+
};
291288

292289
if (options.PersistentDataFileAttachmentPaths != null)
293290
{
@@ -309,9 +306,9 @@ public static BugSplat CreateFromOptions(BugSplatOptions options)
309306
/// <param name="logMessage">logMessage provided by logMessageReceived event that will be used as post description</param>
310307
/// <param name="stackTrace">stackTrace provided by logMessageReceived event</param>
311308
/// <param name="type">type provided by logMessageReceived event</param>
312-
public void LogMessageReceived(string logMessage, string stackTrace, LogType type)
309+
public IEnumerator LogMessageReceived(string logMessage, string stackTrace, LogType type)
313310
{
314-
exceptionReporter.LogMessageReceived(logMessage, stackTrace, type);
311+
yield return exceptionReporter.LogMessageReceived(logMessage, stackTrace, type);
315312
}
316313

317314
/// <summary>
@@ -320,7 +317,7 @@ public void LogMessageReceived(string logMessage, string stackTrace, LogType typ
320317
/// <param name="exception">The Exception that will be serialized and posted to BugSplat</param>
321318
/// <param name="options">Optional parameters that will override the defaults if provided</param>
322319
/// <param name="callback">Optional callback that will be invoked with an HttpResponseMessage after exception is posted to BugSplat</param>
323-
public IEnumerator Post(Exception exception, IReportPostOptions options = null, Action callback = null)
320+
public IEnumerator Post(Exception exception, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null)
324321
{
325322
return exceptionReporter.Post(exception, options, callback);
326323
}

Runtime/Client/DotNetStandardClient.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66

77
namespace BugSplatUnity.Runtime.Client
88
{
9-
internal class DotNetStandardClient : INativeCrashReportClient, IExceptionClient<Task<HttpResponseMessage>>
9+
internal interface IDotNetStandardExceptionClient
10+
{
11+
Task<HttpResponseMessage> Post(string stackTrace, IReportPostOptions options = null);
12+
Task<HttpResponseMessage> Post(Exception ex, IReportPostOptions options = null);
13+
Task<HttpResponseMessage> Post(FileInfo minidumpFileInfo, IReportPostOptions options = null);
14+
}
15+
16+
internal class DotNetStandardClient : INativeCrashReportClient, IDotNetStandardExceptionClient
1017
{
1118
private readonly BugSplatDotNetStandard.BugSplat _bugsplat;
1219

Runtime/Client/IExceptionClient.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

Runtime/Client/WebGLExceptionClient.cs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
using System;
22
using System.Collections;
33
using System.Collections.Generic;
4+
using BugSplatUnity.Runtime.Reporter;
45
using UnityEngine;
56

67
namespace BugSplatUnity.Runtime.Client
78
{
8-
internal class WebGLExceptionClient : IExceptionClient<IEnumerator>
9+
internal interface IWebGlExceptionClient
10+
{
11+
IEnumerator Post(string stackTrace, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null);
12+
IEnumerator Post(Exception ex, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null);
13+
}
14+
15+
internal class WebGLExceptionClient : IWebGlExceptionClient
916
{
1017
private readonly string _database;
1118
private readonly string _application;
@@ -20,17 +27,17 @@ public WebGLExceptionClient(string database, string application, string version)
2027
_version = version;
2128
}
2229

23-
public IEnumerator Post(string stackTrace, IReportPostOptions options = null)
30+
public IEnumerator Post(string stackTrace, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null)
2431
{
25-
return PostException(stackTrace, options);
32+
return PostException(stackTrace, options, callback);
2633
}
2734

28-
public IEnumerator Post(Exception ex, IReportPostOptions options = null)
35+
public IEnumerator Post(Exception ex, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null)
2936
{
30-
return PostException(ex.ToString(), options);
37+
return PostException(ex.ToString(), options, callback);
3138
}
3239

33-
private IEnumerator PostException(string exception, IReportPostOptions options = null)
40+
private IEnumerator PostException(string exception, IReportPostOptions options = null, Action<ExceptionReporterPostResult> callback = null)
3441
{
3542
options = options ?? new ReportPostOptions();
3643

@@ -45,19 +52,37 @@ private IEnumerator PostException(string exception, IReportPostOptions options =
4552
{ "appKey", options.Key },
4653
{ "user", options.User },
4754
{ "callstack", exception },
48-
{ "crashTypeId", $"{(int)options.CrashTypeId}" }
55+
{ "crashTypeId", $"{options.CrashTypeId}" }
4956
};
5057

5158
var request = UnityWebClient.Post(url, formData);
5259
yield return request.SendWebRequest();
5360

61+
var responseCode = request.ResponseCode;
62+
5463
if (!request.Success)
5564
{
5665
Debug.LogError($"BugSplat error: Could not post exception {request.Error}");
66+
67+
callback?.Invoke(new ExceptionReporterPostResult() {
68+
Uploaded = false,
69+
Exception = exception,
70+
Message = $"BugSplat upload failed with code {responseCode}"
71+
});
72+
5773
yield break;
5874
}
5975

60-
Debug.Log($"BugSplat info: status {request.ResponseCode}\n {request.DownloadHandler.Text}");
76+
var responseText = request.DownloadHandler.Text;
77+
var response = JsonUtility.FromJson<BugSplatResponse>(responseText);
78+
Debug.Log($"BugSplat info: status {responseCode}\n {responseText}");
79+
80+
callback?.Invoke(new ExceptionReporterPostResult() {
81+
Uploaded = true,
82+
Exception = exception,
83+
Message = "Crash successfully uploaded to BugSplat!",
84+
Response = response
85+
});
6186
}
6287
}
6388
}

Runtime/Manager/BugSplatManager.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using BugSplatUnity.Runtime.Client;
23
using UnityEngine;
34

@@ -15,15 +16,30 @@ public sealed class BugSplatManager : MonoBehaviour
1516

1617
[SerializeField]
1718
[Tooltip("Register BugSplat to capture LogType.Exceptions on initialization.")]
18-
private bool registerLogMessageRecieved;
19+
private bool registerLogMessageReceived = true;
1920

20-
private BugSplatManagerImpl _impl;
21-
public BugSplat BugSplat => _impl.BugSplat;
21+
private BugSplatRef bugsplatRef;
22+
public BugSplat BugSplat => bugsplatRef.BugSplat;
2223

2324
private void Awake()
2425
{
25-
_impl = new BugSplatManagerImpl(bugSplatOptions, registerLogMessageRecieved, dontDestroyManagerOnSceneLoad, gameObject);
26-
_impl.Instantiate();
26+
if (bugSplatOptions == null)
27+
{
28+
throw new ArgumentException("BugSplat error: BugSplatOptions is null! BugSplat will not be initialized.");
29+
}
30+
31+
var bugsplat = BugSplat.CreateFromOptions(bugSplatOptions);
32+
bugsplatRef = new BugSplatRef(bugsplat);
33+
34+
if (registerLogMessageReceived)
35+
{
36+
Application.logMessageReceived += (logMessage, stackTrace, type) => StartCoroutine(bugsplat.LogMessageReceived(logMessage, stackTrace, type));
37+
}
38+
39+
if (dontDestroyManagerOnSceneLoad)
40+
{
41+
DontDestroyOnLoad(this);
42+
}
2743
}
2844
}
2945
}

0 commit comments

Comments
 (0)