Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 237 additions & 2 deletions Runtime/BacktraceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Backtrace.Unity.Runtime.Native;
using Backtrace.Unity.Services;
using Backtrace.Unity.Types;
using Backtrace.Unity.WebGL;
using System;
using System.Collections;
using System.Collections.Generic;
Expand Down Expand Up @@ -131,6 +132,17 @@ internal System.Random Random
/// </summary>
private HashSet<string> _clientReportAttachments;

#if UNITY_WEBGL
private WebGLOfflineDatabase _webglOfflineDatabase;
private Coroutine _webglOfflineReplayCoroutine;
private bool _webglOfflineReplayInProgress;

// WebGL builds may have multiple BacktraceClient instances if DestroyOnLoad is enabled.
// Ensure only one instance performs offline queue replay to avoid duplicate sends.
private static BacktraceClient _webglOfflineReplayOwner;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a static Backtrace instance available here.

#endif
Comment on lines +135 to +143
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be moved to BacktraceDatabase - BacktraceDatabase offerts the same Untiy scope.



/// <summary>
/// Attribute object accessor
/// </summary>
Expand Down Expand Up @@ -518,6 +530,9 @@ public static BacktraceClient Initialize(string url, Dictionary<string, string>
public void OnDisable()
{
Enabled = false;
#if UNITY_WEBGL
StopWebGLOfflineReplay();
#endif
}

public void Refresh()
Expand Down Expand Up @@ -553,6 +568,11 @@ public void Refresh()
#endif
);
BacktraceApi.EnablePerformanceStatistics = Configuration.PerformanceStatistics;
#if UNITY_WEBGL
// JS page lifecycle hooks
BacktraceWebGLSync.TryInstallPageLifecycleHooks();
#endif


if (!Configuration.DestroyOnLoad)
{
Expand All @@ -579,6 +599,9 @@ public void Refresh()
}
}
}
#if UNITY_WEBGL
InitializeWebGLOfflineSupport();
#endif
if (Database != null)
{
// send minidump files generated by unity engine or unity game, not captured by Windows native integration
Expand All @@ -598,6 +621,178 @@ public void Refresh()
AttributeProvider.AddDynamicAttributeProvider(_nativeClient);
}
}
#if UNITY_WEBGL
private void InitializeWebGLOfflineSupport()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be abstracted in the BacktraceDatabase implementation - we have the Start/Awake support there.

{
// WebGL offline queue is only active when Backtrace offline database is enabled in configuration.
if (Configuration == null || !Configuration.Enabled)
{
return;
}

if (_webglOfflineDatabase == null)
{
_webglOfflineDatabase = new WebGLOfflineDatabase(Configuration);
}

// Clean up corrupted/invalid entries and enforce size/count bounds on start.
_webglOfflineDatabase.Compact();

// auto-send stored reports if AutoSendMode is enabled.
if (!Configuration.AutoSendMode)
{
return;
}

// Avoid multiple BacktraceClient instances replaying the same persisted queue.
if (_webglOfflineReplayOwner != null && _webglOfflineReplayOwner != this)
{
return;
}
_webglOfflineReplayOwner = this;

if (_webglOfflineReplayCoroutine == null)
{
_webglOfflineReplayCoroutine = StartCoroutine(WebGLOfflineReplayLoop());
}
}

private IEnumerator WebGLOfflineReplayLoop()
{
// Wait one frame to allow other initialization to complete.
yield return null;

while (Enabled && Configuration != null && Configuration.Enabled && Configuration.AutoSendMode)
{
if (_webglOfflineDatabase != null &&
!_webglOfflineDatabase.IsEmpty &&
BacktraceApi != null &&
RequestHandler == null)
{
yield return SendCachedWebGLReports();
}

var retryInterval = Configuration.RetryInterval > 0
? Configuration.RetryInterval
: BacktraceConfiguration.DefaultRetryInterval;

yield return new WaitForSecondsRealtime(retryInterval);
}

_webglOfflineReplayCoroutine = null;
_webglOfflineReplayInProgress = false;
}

/// <summary>
/// Send any reports cached in PlayerPrefs while the WebGL client was offline and the BacktraceDatabase was not available or disabled.
/// </summary>
private IEnumerator SendCachedWebGLReports()
{
if (_webglOfflineReplayInProgress)
{
yield break;
}

_webglOfflineReplayInProgress = true;

try
{
if (_webglOfflineDatabase == null || BacktraceApi == null || Configuration == null)
{
yield break;
}

// RequestHandler overrides normal send path. We can't replay stored JSON through it.
if (RequestHandler != null)
{
yield break;
}

var retryOrder = Configuration.RetryOrder;
var retryLimit = Mathf.Max(1, Configuration.RetryLimit);

while (_webglOfflineDatabase.TryPeek(retryOrder, out var record))
{
if (record == null)
{
yield break;
}

// Respect client-side report rate limiting.
if (_reportLimitWatcher != null && !_reportLimitWatcher.WatchReport(DateTimeHelper.Timestamp()))
{
yield break;
}

// Give up on records that exceeded retry limit.
if (record.attempts >= retryLimit)
{
_webglOfflineDatabase.Remove(record.uuid);
yield return null;
continue;
}

var queryAttributes = new Dictionary<string, string>();
if (record.deduplication != 0)
{
queryAttributes["_mod_duplicate"] = record.deduplication.ToString(CultureInfo.InvariantCulture);
}

BacktraceResult sendResult = null;
yield return BacktraceApi.Send(
record.json,
record.attachments ?? new string[0],
queryAttributes,
result => sendResult = result);

if (sendResult == null)
{
_webglOfflineDatabase.IncrementAttempts(record.uuid);
yield break;
}

if (sendResult.Status == BacktraceResultStatus.Ok || sendResult.Status == BacktraceResultStatus.Empty)
{
_webglOfflineDatabase.Remove(record.uuid);
yield return null;
continue;
}

// Treat all non-success status as retryable attempts for the WebGL offline replay.
if (sendResult.Status == BacktraceResultStatus.ServerError ||
sendResult.Status == BacktraceResultStatus.NetworkError ||
sendResult.Status == BacktraceResultStatus.LimitReached)
{
_webglOfflineDatabase.IncrementAttempts(record.uuid);
yield break;
}

yield break;
}
}
finally
{
_webglOfflineReplayInProgress = false;
}
}

private void StopWebGLOfflineReplay()
{
if (_webglOfflineReplayCoroutine != null)
{
StopCoroutine(_webglOfflineReplayCoroutine);
_webglOfflineReplayCoroutine = null;
}

_webglOfflineReplayInProgress = false;

if (_webglOfflineReplayOwner == this)
{
_webglOfflineReplayOwner = null;
}
}
#endif


public bool EnableBreadcrumbsSupport()
{
Expand Down Expand Up @@ -673,6 +868,10 @@ private void StartupMetrics()

private void OnApplicationQuit()
{
#if UNITY_WEBGL
StopWebGLOfflineReplay();
BacktraceWebGLSync.TrySyncFileSystem(true);
#endif
if (_nativeClient != null)
{
_nativeClient.Disable();
Expand Down Expand Up @@ -721,6 +920,10 @@ private void LateUpdate()
private void OnDestroy()
{
Enabled = false;
#if UNITY_WEBGL
StopWebGLOfflineReplay();
BacktraceWebGLSync.TrySyncFileSystem(true);
#endif
if (_breadcrumbs != null)
{
_breadcrumbs.FromMonoBehavior("Backtrace Client: OnDestroy", LogType.Warning, null);
Expand Down Expand Up @@ -867,7 +1070,8 @@ private IEnumerator CollectDataAndSend(BacktraceReport report, Action<BacktraceR
}
BacktraceDatabaseRecord record = null;

if (Database != null && Database.Enabled())
bool databaseEnabled = Database != null && Database.Enabled();
if (databaseEnabled)
{
yield return WaitForFrame.Wait();
if (EnablePerformanceStatistics)
Expand All @@ -880,6 +1084,9 @@ record = Database.Add(data);
{
//Extend backtrace data with additional attachments from backtrace database
data = record.BacktraceData;
#if UNITY_WEBGL
BacktraceWebGLSync.TrySyncFileSystem();
#endif
if (EnablePerformanceStatistics)
{
stopWatch.Stop();
Expand Down Expand Up @@ -938,11 +1145,22 @@ record = Database.Add(data);
if (record != null)
{
record.Unlock();
if (Database != null && result.Status != BacktraceResultStatus.ServerError && result.Status != BacktraceResultStatus.NetworkError)
if (databaseEnabled && result.Status != BacktraceResultStatus.ServerError && result.Status != BacktraceResultStatus.NetworkError)
{
Database.Delete(record);
}
}
#if UNITY_WEBGL
if (!databaseEnabled &&
Configuration != null &&
Configuration.Enabled &&
_webglOfflineDatabase != null &&
(result.Status == BacktraceResultStatus.ServerError || result.Status == BacktraceResultStatus.NetworkError))
{
_webglOfflineDatabase.Enqueue(data.Uuid, json, data.Attachments, data.Deduplication);
}
#endif

//check if there is more errors to send
//handle inner exception
HandleInnerException(report);
Expand Down Expand Up @@ -1073,6 +1291,23 @@ internal void OnApplicationPause(bool pause)
{
_nativeClient.PauseAnrThread(pause);
}
#if UNITY_WEBGL
if (pause)
{
BacktraceWebGLSync.TrySyncFileSystem(true);
}
#endif

}

private void OnApplicationFocus(bool hasFocus)
{
#if UNITY_WEBGL
if (!hasFocus)
{
BacktraceWebGLSync.TrySyncFileSystem(true);
}
#endif
}

internal void HandleUnityBackgroundException(string message, string stackTrace, LogType type)
Expand Down
Loading
Loading