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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions package-dev/Plugins/Switch/sentry_native_stubs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/Sentry.Unity.Native/SentryNativeSwitch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Whether the console currently has a usable network.
/// </summary>
/// <remarks>
/// <c>Application.internetReachability</c> 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.
/// </remarks>
internal static bool IsNetworkAvailable() => sentry_switch_utils_is_network_available() == 1;

/// <summary>
/// Configures the native support for Nintendo Switch.
/// </summary>
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Unity/Integrations/IApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal interface IApplication
string UnityVersion { get; }
string PersistentDataPath { get; }
RuntimePlatform Platform { get; }
NetworkReachability InternetReachability { get; }
}

/// <summary>
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ internal string? DefaultUserId
/// </summary>
internal Action? NativeSupportCloseCallback { get; set; } = null;

internal Func<bool>? NetworkAvailabilityProbe { get; set; } = null;

internal List<string> SdkIntegrationNames { get; set; } = new();

internal ISentryUnityInfo UnityInfo { get; private set; }
Expand Down
63 changes: 61 additions & 2 deletions src/Sentry.Unity/UnityWebRequestTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Comment thread
bitsandfoxes marked this conversation as resolved.
}

using var processedEnvelope = ProcessEnvelope(envelope);
if (processedEnvelope.Items.Count > 0)
{
Expand All @@ -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)
{
Expand All @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs
Original file line number Diff line number Diff line change
@@ -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());

/// <summary>
/// A coroutine that yields has created a <see cref="UnityEngine.Networking.UnityWebRequest"/>,
/// which is what raises the system connection dialog on platforms like the Nintendo Switch.
/// </summary>
[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.");
}

/// <summary>
/// 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.
/// </summary>
[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.");
}
}
1 change: 1 addition & 0 deletions test/SharedClasses/TestApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading