diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1271dd85f..a13739050 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@
### Fixes
- IL2CPP line numbers now work on Android x86/x86_64 builds. il2cpp fails to report the image UUID there, so the SDK falls back to looking the debug image up by name ([#2817](https://github.com/getsentry/sentry-unity/pull/2817))
+- When targeting WebGL or Nintendo Switch without native support, `UnityWebRequestTransport` no longer opens a connection while the platform reports no network, and backs off exponentially (1s up to 300s) after a connection error instead of retrying on every envelope. ([#2833](https://github.com/getsentry/sentry-unity/pull/2833))
+- When targeting Nintendo Switch, the SDK now utilizes sentry-switch to poll the network status before sending. ([#2833](https://github.com/getsentry/sentry-unity/pull/2833))
### Dependencies
diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c b/package-dev/Plugins/Switch/sentry_native_stubs.c
index 04836e9e0..57ffbf7d8 100644
--- a/package-dev/Plugins/Switch/sentry_native_stubs.c
+++ b/package-dev/Plugins/Switch/sentry_native_stubs.c
@@ -408,6 +408,12 @@ const char* sentry_switch_utils_get_default_user_id(void)
return "";
}
+int sentry_switch_utils_is_network_available(void)
+{
+ /* No native SDK to ask - assume usable and let the transport's backoff take over */
+ return 1;
+}
+
/*
* =============================================================================
* Utility Functions
diff --git a/src/Sentry.Unity.Native/SentryNativeSwitch.cs b/src/Sentry.Unity.Native/SentryNativeSwitch.cs
index 6fab13909..b01a93ce5 100644
--- a/src/Sentry.Unity.Native/SentryNativeSwitch.cs
+++ b/src/Sentry.Unity.Native/SentryNativeSwitch.cs
@@ -28,8 +28,20 @@ public static class SentryNativeSwitch
[DllImport("__Internal")]
private static extern IntPtr sentry_switch_utils_get_default_user_id();
+ [DllImport("__Internal")]
+ private static extern int sentry_switch_utils_is_network_available();
+
private static IDiagnosticLogger? Logger;
+ ///
+ /// Whether the console currently has a usable network.
+ ///
+ ///
+ /// Application.internetReachability reports the console as reachable while it is offline,
+ /// and every send attempted in that state raises the system's "connect to the internet" dialog.
+ ///
+ internal static bool IsNetworkAvailable() => sentry_switch_utils_is_network_available() == 1;
+
///
/// Configures the native support for Nintendo Switch.
///
@@ -72,6 +84,9 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf
return;
}
+ Logger?.LogDebug("Using the native SDK to determine network availability.");
+ options.NetworkAvailabilityProbe = IsNetworkAvailable;
+
Logger?.LogDebug("Mounting temporary storage for sentry-switch.");
if (sentry_switch_utils_mount() != 1)
diff --git a/src/Sentry.Unity/Integrations/IApplication.cs b/src/Sentry.Unity/Integrations/IApplication.cs
index 805c63543..cca143b4d 100644
--- a/src/Sentry.Unity/Integrations/IApplication.cs
+++ b/src/Sentry.Unity/Integrations/IApplication.cs
@@ -17,6 +17,7 @@ internal interface IApplication
string UnityVersion { get; }
string PersistentDataPath { get; }
RuntimePlatform Platform { get; }
+ NetworkReachability InternetReachability { get; }
}
///
@@ -54,6 +55,8 @@ private ApplicationAdapter()
public RuntimePlatform Platform => Application.platform;
+ public NetworkReachability InternetReachability => Application.internetReachability;
+
private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
=> LogMessageReceived?.Invoke(condition, stackTrace, type);
diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs
index 1cb15ad5b..749339cba 100644
--- a/src/Sentry.Unity/SentryUnityOptions.cs
+++ b/src/Sentry.Unity/SentryUnityOptions.cs
@@ -493,6 +493,8 @@ internal string? DefaultUserId
///
internal Action? NativeSupportCloseCallback { get; set; } = null;
+ internal Func? NetworkAvailabilityProbe { get; set; } = null;
+
internal List SdkIntegrationNames { get; set; } = new();
internal ISentryUnityInfo UnityInfo { get; private set; }
diff --git a/src/Sentry.Unity/UnityWebRequestTransport.cs b/src/Sentry.Unity/UnityWebRequestTransport.cs
index c19a18d8c..1e92d5d46 100644
--- a/src/Sentry.Unity/UnityWebRequestTransport.cs
+++ b/src/Sentry.Unity/UnityWebRequestTransport.cs
@@ -6,7 +6,11 @@
using System.Threading.Tasks;
using Sentry.Extensibility;
using Sentry.Http;
+using Sentry.Internal;
+using Sentry.Internal.Extensions;
using Sentry.Protocol.Envelopes;
+using Sentry.Unity.Integrations;
+using UnityEngine;
using UnityEngine.Networking;
namespace Sentry.Unity;
@@ -50,14 +54,31 @@ private IEnumerator SendAndTrack(Envelope envelope)
internal class UnityWebRequestTransport : HttpTransportBase
{
private readonly SentryUnityOptions _options;
+ private readonly IApplication _application;
- public UnityWebRequestTransport(SentryUnityOptions options)
+ private const double MaxBackoffSeconds = 300.0;
+ private double _backoffSeconds;
+ private double _retryAfterRealtime;
+
+ public UnityWebRequestTransport(SentryUnityOptions options, IApplication? application = null)
: base(options)
- => _options = options;
+ {
+ _options = options;
+ _application = application ?? ApplicationAdapter.Instance;
+ }
// adapted HttpTransport.SendEnvelopeAsync()
internal IEnumerator SendEnvelopeAsync(Envelope envelope)
{
+ // This transport opens a connection per envelope, causing a prompt to appear on platforms
+ // like Nintendo Switch while offline.
+ if (!IsNetworkAvailable() || Time.realtimeSinceStartupAsDouble < _retryAfterRealtime)
+ {
+ _options.LogDebug("Network unavailable or backing off. Dropping envelope instead of attempting to send.");
+ _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope);
+ yield break;
+ }
+
using var processedEnvelope = ProcessEnvelope(envelope);
if (processedEnvelope.Items.Count > 0)
{
@@ -66,6 +87,20 @@ internal IEnumerator SendEnvelopeAsync(Envelope envelope)
var www = CreateWebRequest(httpRequest);
yield return www.SendWebRequest();
+ if (www.result == UnityWebRequest.Result.ConnectionError)
+ {
+ _backoffSeconds = Math.Min(MaxBackoffSeconds, _backoffSeconds > 0.0 ? _backoffSeconds * 2 : 1.0);
+ _retryAfterRealtime = Time.realtimeSinceStartupAsDouble + _backoffSeconds;
+
+ _options.LogWarning("Failed to send request: {0}. Backing off for {1:F1}s.", www.error, _backoffSeconds);
+ _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, processedEnvelope);
+ RestoreClientReports(processedEnvelope);
+ yield break;
+ }
+
+ _backoffSeconds = 0.0;
+ _retryAfterRealtime = 0.0;
+
var response = GetResponse(www);
if (response is not null)
{
@@ -74,6 +109,30 @@ internal IEnumerator SendEnvelopeAsync(Envelope envelope)
}
}
+ // ProcessEnvelope drains the recorder into a client report item. Restore those counts instead of
+ // losing them along with the envelope we failed to send.
+ private void RestoreClientReports(Envelope envelope)
+ {
+ foreach (var item in envelope.Items)
+ {
+ if (item.TryGetType() == EnvelopeItem.TypeValueClientReport &&
+ item.Payload is JsonSerializable { Source: ClientReport report })
+ {
+ _options.ClientReportRecorder.Load(report);
+ }
+ }
+ }
+
+ private bool IsNetworkAvailable()
+ {
+ if (_options.NetworkAvailabilityProbe is { } probe)
+ {
+ return probe();
+ }
+
+ return _application.InternetReachability != NetworkReachability.NotReachable;
+ }
+
private UnityWebRequest CreateWebRequest(HttpRequestMessage message)
{
using var contentStream = ReadStreamFromHttpContent(message.Content);
diff --git a/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs b/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs
new file mode 100644
index 000000000..a25e9c8d8
--- /dev/null
+++ b/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs
@@ -0,0 +1,79 @@
+using NUnit.Framework;
+using Sentry.Protocol.Envelopes;
+using Sentry.Unity.Tests.Stubs;
+using UnityEngine;
+
+namespace Sentry.Unity.Tests;
+
+[TestFixture]
+public class UnityWebRequestTransportTests
+{
+ private static SentryUnityOptions CreateOptions() => new()
+ {
+ Dsn = SentryTests.TestDsn
+ };
+
+ private static Envelope CreateEnvelope() => Envelope.FromEvent(new SentryEvent());
+
+ ///
+ /// A coroutine that yields has created a ,
+ /// which is what raises the system connection dialog on platforms like the Nintendo Switch.
+ ///
+ [Test]
+ public void SendEnvelopeAsync_NetworkNotReachable_DoesNotOpenAConnection()
+ {
+ var application = new TestApplication { InternetReachability = NetworkReachability.NotReachable };
+ var transport = new UnityWebRequestTransport(CreateOptions(), application);
+
+ var enumerator = transport.SendEnvelopeAsync(CreateEnvelope());
+
+ Assert.IsFalse(enumerator.MoveNext(), "The transport attempted to send while offline.");
+ }
+
+ [Test]
+ public void SendEnvelopeAsync_NetworkReachable_AttemptsToSend()
+ {
+ var application = new TestApplication
+ {
+ InternetReachability = NetworkReachability.ReachableViaLocalAreaNetwork
+ };
+ var transport = new UnityWebRequestTransport(CreateOptions(), application);
+
+ var enumerator = transport.SendEnvelopeAsync(CreateEnvelope());
+
+ Assert.IsTrue(enumerator.MoveNext(), "The transport did not attempt to send while online.");
+ }
+
+ ///
+ /// The probe has the final say: on Nintendo Switch reachability claims the console is reachable
+ /// while it is offline, which is the state the dialog gets raised in.
+ ///
+ [Test]
+ public void SendEnvelopeAsync_ProbeReportsNoNetwork_OverridesReachability()
+ {
+ var application = new TestApplication
+ {
+ InternetReachability = NetworkReachability.ReachableViaLocalAreaNetwork
+ };
+ var options = CreateOptions();
+ options.NetworkAvailabilityProbe = () => false;
+ var transport = new UnityWebRequestTransport(options, application);
+
+ var enumerator = transport.SendEnvelopeAsync(CreateEnvelope());
+
+ Assert.IsFalse(enumerator.MoveNext(), "The transport ignored the platform's network probe.");
+ }
+
+ [Test]
+ public void SendEnvelopeAsync_ProbeReportsNetwork_OverridesReachability()
+ {
+ var application = new TestApplication { InternetReachability = NetworkReachability.NotReachable };
+ var options = CreateOptions();
+ options.NetworkAvailabilityProbe = () => true;
+ var transport = new UnityWebRequestTransport(options, application);
+
+ var enumerator = transport.SendEnvelopeAsync(CreateEnvelope());
+
+ Assert.IsTrue(enumerator.MoveNext(), "The transport ignored the platform's network probe.");
+ }
+}
diff --git a/test/SharedClasses/TestApplication.cs b/test/SharedClasses/TestApplication.cs
index 45ed584f9..2051fc3c0 100644
--- a/test/SharedClasses/TestApplication.cs
+++ b/test/SharedClasses/TestApplication.cs
@@ -41,6 +41,7 @@ public TestApplication(
public string UnityVersion { get; set; }
public string PersistentDataPath { get; set; }
public RuntimePlatform Platform { get; set; }
+ public NetworkReachability InternetReachability { get; set; } = NetworkReachability.ReachableViaLocalAreaNetwork;
private void OnLogMessageReceived(string condition, string stacktrace, LogType type)
=> LogMessageReceived?.Invoke(condition, stacktrace, type);