From b3b8645cc58546128bb9afbd5a5ab70bb349f6c7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 3 Sep 2026 11:30:02 +0200 Subject: [PATCH 1/7] fix(switch): stop the transport from reconnecting blindly while offline UnityWebRequestTransport opens a connection per envelope. On Nintendo Switch every attempt made while the console is offline raises the system "connect to the internet" dialog, and because logs and metrics each flush on a 5s timer that turned into a prompt every couple of seconds. A connection error previously only logged a warning and returned null, which also skipped the rate-limit handling in HttpTransportBase, so nothing anywhere recorded that the network was down. Add two guards to SendEnvelopeAsync: skip sending entirely while the platform reports NotReachable, and back off exponentially (1s up to 60s) after a connection error, resetting on the first success. The backoff is what covers platforms whose reachability cannot be trusted, and the case where the network drops after startup. Both paths record DiscardReason.NetworkError so client reports still account for the drops. Reachability goes through IApplication rather than UnityEngine.Application so the behaviour is testable. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- CHANGELOG.md | 4 + src/Sentry.Unity/Integrations/IApplication.cs | 3 + src/Sentry.Unity/UnityWebRequestTransport.cs | 99 ++++++++++++++++++- .../UnityWebRequestTransportTests.cs | 47 +++++++++ test/SharedClasses/TestApplication.cs | 1 + 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bde06bde..0a2c70910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- The `UnityWebRequestTransport` no longer opens a connection while the platform reports no network, and backs off exponentially (1s up to 60s) after a connection error instead of retrying on every envelope. On Nintendo Switch each send attempt made while offline raises the system's "connect to the internet" dialog, so a game with logs or metrics enabled produced a prompt every few seconds ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) + ### Dependencies - Bump .NET SDK from v6.8.0 to v6.9.0 ([#2815](https://github.com/getsentry/sentry-unity/pull/2815)) 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/UnityWebRequestTransport.cs b/src/Sentry.Unity/UnityWebRequestTransport.cs index c19a18d8c..2c502f388 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; @@ -51,13 +55,36 @@ internal class UnityWebRequestTransport : HttpTransportBase { private readonly SentryUnityOptions _options; - public UnityWebRequestTransport(SentryUnityOptions options) + // This transport opens a connection per envelope. On platforms that raise a system dialog + // when the game reaches for an unavailable network - Nintendo Switch prompts to connect to + // the internet - every attempt made while offline puts that dialog on screen. A game with + // logs and metrics enabled flushes an envelope every few seconds, so a blind retry per + // envelope turns into a steady stream of prompts. Two guards bound that: the reachability + // check skips sending altogether while the platform reports no network, and the backoff + // spaces out attempts on platforms where reachability cannot be trusted. + private const double InitialBackoffSeconds = 1.0; + private const double MaxBackoffSeconds = 60.0; + + private readonly IApplication _application; + + private int _consecutiveConnectionErrors; + 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) { + if (!CanAttemptSend(envelope)) + { + yield break; + } + using var processedEnvelope = ProcessEnvelope(envelope); if (processedEnvelope.Items.Count > 0) { @@ -66,6 +93,14 @@ internal IEnumerator SendEnvelopeAsync(Envelope envelope) var www = CreateWebRequest(httpRequest); yield return www.SendWebRequest(); + if (www.result == UnityWebRequest.Result.ConnectionError) + { + OnConnectionError(www, processedEnvelope); + yield break; + } + + OnConnectionSucceeded(); + var response = GetResponse(www); if (response is not null) { @@ -74,6 +109,66 @@ internal IEnumerator SendEnvelopeAsync(Envelope envelope) } } + /// + /// Whether it is worth opening a connection for this envelope. Envelopes dropped here are + /// recorded as discarded so client reports still account for them. + /// + private bool CanAttemptSend(Envelope envelope) + { + if (_application.InternetReachability == NetworkReachability.NotReachable) + { + _options.LogDebug("No network available. Dropping envelope instead of attempting to send."); + _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope); + return false; + } + + if (_consecutiveConnectionErrors > 0) + { + var remaining = _retryAfterRealtime - Time.realtimeSinceStartupAsDouble; + if (remaining > 0.0) + { + _options.LogDebug( + "Backing off after {0} failed connection attempt(s). Dropping envelope, retrying in {1:F1}s.", + _consecutiveConnectionErrors, remaining); + _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope); + return false; + } + } + + return true; + } + + private void OnConnectionError(UnityWebRequest www, Envelope envelope) + { + _consecutiveConnectionErrors++; + + var backoff = Math.Min( + MaxBackoffSeconds, + InitialBackoffSeconds * Math.Pow(2, _consecutiveConnectionErrors - 1)); + _retryAfterRealtime = Time.realtimeSinceStartupAsDouble + backoff; + + // Reachability is logged here because a platform reporting itself reachable while the + // connection fails is exactly the case the backoff exists to cover. + var backoffDetail = $"{backoff:F1}s (consecutive failure #{_consecutiveConnectionErrors})"; + _options.LogWarning( + "Failed to send request: {0}. Reachability reported as {1}. Backing off for {2}.", + www.error, _application.InternetReachability, backoffDetail); + + _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope); + } + + private void OnConnectionSucceeded() + { + if (_consecutiveConnectionErrors == 0) + { + return; + } + + _options.LogDebug("Connection restored after {0} failed attempt(s).", _consecutiveConnectionErrors); + _consecutiveConnectionErrors = 0; + _retryAfterRealtime = 0.0; + } + 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..10cd48b8b --- /dev/null +++ b/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs @@ -0,0 +1,47 @@ +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()); + + /// + /// The coroutine has to bail before its first yield. Anything else means a + /// got created, 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."); + } +} 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); From 97c40b41a56b46e4937d561475b7a0f13bb82ccd Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 3 Sep 2026 18:36:46 +0200 Subject: [PATCH 2/7] fix(switch): stop the diagnostic logger from throwing while logging Callers log from inside catch blocks that have already handled the failure. UnityLogger interpolated the exception directly, so anything that threw while being stringified escaped from within the handler and turned a handled error into a fatal one. On Nintendo Switch that is what happened: with DisableFileWrite set, resolving the installation ID falls through to NetworkInterface.GetAllNetworkInterfaces(), whose static initializer throws. Hub.ConfigureScope caught it and logged it, but stringifying that exception threw again, escaping the handler and aborting SDK initialization partway through registering integrations. Every Unity integration after GlobalRootScopeIntegration was left unregistered, so nothing was captured and no envelope was ever produced. Format the message and render the exception defensively, falling back to the type and message when the stack trace cannot be read, and never let the write to Unity's logger propagate. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- src/Sentry.Unity/UnityLogger.cs | 54 ++++++++++++++++++++- test/Sentry.Unity.Tests/UnityLoggerTests.cs | 49 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/Sentry.Unity/UnityLogger.cs b/src/Sentry.Unity/UnityLogger.cs index 4f7ef6b0a..d0d667856 100644 --- a/src/Sentry.Unity/UnityLogger.cs +++ b/src/Sentry.Unity/UnityLogger.cs @@ -30,7 +30,59 @@ public void Log(SentryLevel logLevel, string? message, Exception? exception = nu return; } - _logger.Log(GetUnityLogType(logLevel), LogTag, $"({logLevel.ToString()}) {Format(message, args)} {exception}"); + // A diagnostic logger must never take its caller down with it. Callers routinely log from + // inside a `catch` that already handled the failure - if logging throws there, a handled + // error turns into a fatal one. That is not hypothetical: on Nintendo Switch some + // exceptions throw again while their stack trace is being stringified, which aborted SDK + // initialization from inside the handler that had already dealt with the original problem. + string logMessage; + try + { + logMessage = $"({logLevel.ToString()}) {Format(message, args)} {Describe(exception)}"; + } + catch (Exception e) + { + logMessage = $"({logLevel.ToString()}) Failed to format log message: {e.GetType().Name}"; + } + + try + { + _logger.Log(GetUnityLogType(logLevel), LogTag, logMessage); + } + catch + { + // Reporting a logging failure would have to go through the very thing that just failed. + } + } + + /// + /// Renders an exception without trusting , which walks the + /// stack trace and can throw on platforms with limited stack trace support. + /// + private static string Describe(Exception? exception) + { + if (exception is null) + { + return Empty; + } + + try + { + return exception.ToString(); + } + catch (Exception e) + { + // Type and message are cheap to read and usually survive when the stack trace does not. + try + { + return $"{exception.GetType().FullName}: {exception.Message}" + + $" (stack trace unavailable: {e.GetType().Name})"; + } + catch + { + return ""; + } + } } internal static LogType GetUnityLogType(SentryLevel logLevel) diff --git a/test/Sentry.Unity.Tests/UnityLoggerTests.cs b/test/Sentry.Unity.Tests/UnityLoggerTests.cs index daad31632..bed5f5167 100644 --- a/test/Sentry.Unity.Tests/UnityLoggerTests.cs +++ b/test/Sentry.Unity.Tests/UnityLoggerTests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using Sentry.Unity.Tests.SharedClasses; using UnityEngine; @@ -50,4 +51,52 @@ public void Log_SetsTag() // The format is: "(logType, tag, message)" StringAssert.AreEqualIgnoringCase(UnityLogger.LogTag, testLogger.Logs[0].Item2); } + + /// + /// Callers log from inside `catch` blocks that already handled the failure. If the logger + /// throws there, a handled error becomes a fatal one - which is how SDK initialization aborted + /// on Nintendo Switch, where stringifying certain exceptions throws again. + /// + [Test] + public void Log_ExceptionThrowsWhileBeingStringified_DoesNotPropagateAndStillLogs() + { + var testLogger = new UnityTestLogger(); + var logger = new UnityLogger(new SentryOptions { DiagnosticLevel = SentryLevel.Debug }, testLogger); + + Assert.DoesNotThrow(() => + logger.Log(SentryLevel.Error, "Something failed", new ThrowingToStringException())); + + Assert.AreEqual(1, testLogger.Logs.Count); + var message = testLogger.Logs[0].Item3; + StringAssert.Contains("Something failed", message); + StringAssert.Contains(nameof(ThrowingToStringException), message); + } + + [Test] + public void Log_MessageAndArgumentsDoNotMatch_DoesNotPropagate() + { + var testLogger = new UnityTestLogger(); + var logger = new UnityLogger(new SentryOptions { DiagnosticLevel = SentryLevel.Debug }, testLogger); + + // More placeholders than arguments - string.Format throws on this. + Assert.DoesNotThrow(() => logger.Log(SentryLevel.Debug, "{0} {1} {2}", null, "only-one")); + + Assert.AreEqual(1, testLogger.Logs.Count); + } + + private sealed class ThrowingToStringException : Exception + { + public ThrowingToStringException() + { } + + public ThrowingToStringException(string message) : base(message) + { } + + public ThrowingToStringException(string message, Exception innerException) : base(message, innerException) + { } + + public override string ToString() => throw new InvalidOperationException("cannot stringify"); + + public override string StackTrace => throw new InvalidOperationException("no stack trace here"); + } } From 66ed224bdc910a4bc80866ecd7c676497ab4e46b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 4 Sep 2026 12:25:31 +0200 Subject: [PATCH 3/7] fix(switch2): enable native support on Nintendo Switch 2 Switch 2 reached SentryNativeSwitch.Configure but bailed out of it with "Native support is disabled for 'Switch2'": IsNativeSupportEnabled had no case for the platform, so it fell through to the default and returned false. Native support could never initialize, no matter how the project was configured. Switch 2 shares the Switch implementation, so it now follows SwitchNativeSupportEnabled, and the existing Switch assembly is shipped to it by enabling that platform on the assembly's plugin metadata - which is what decides where an assembly goes, so no separate Switch 2 assembly is needed. RuntimePlatform.Switch2 only exists in Unity 6000.3 and newer while the SDK still supports 2021.3, and these assemblies compile against a single Unity version, so the platform is matched by name instead of by enum member - referencing the member directly would stop the SDK building against the editors that predate it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- .../Sentry.Unity.Native.Switch.dll.meta | 4 +++ .../SentryUnityOptionsExtensions.cs | 18 +++++++++++ .../SentryUnityOptionsExtensionsTests.cs | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/package-dev/Runtime/Sentry.Unity.Native.Switch.dll.meta b/package-dev/Runtime/Sentry.Unity.Native.Switch.dll.meta index 25c262827..927a6240c 100644 --- a/package-dev/Runtime/Sentry.Unity.Native.Switch.dll.meta +++ b/package-dev/Runtime/Sentry.Unity.Native.Switch.dll.meta @@ -26,6 +26,7 @@ PluginImporter: Exclude OSXUniversal: 1 Exclude PS5: 1 Exclude Switch: 0 + Exclude Switch2: 0 Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 @@ -50,6 +51,9 @@ PluginImporter: Switch: enabled: 1 settings: {} + Switch2: + enabled: 1 + settings: {} Win: enabled: 0 settings: diff --git a/src/Sentry.Unity/SentryUnityOptionsExtensions.cs b/src/Sentry.Unity/SentryUnityOptionsExtensions.cs index cf2dc13dc..de5553103 100644 --- a/src/Sentry.Unity/SentryUnityOptionsExtensions.cs +++ b/src/Sentry.Unity/SentryUnityOptionsExtensions.cs @@ -49,9 +49,27 @@ internal static bool IsValid(this SentryUnityOptions options) return true; } + /// + /// RuntimePlatform.Switch2 was only added in Unity 6000.3. This assembly is compiled + /// against a single Unity version while the SDK still supports 2021.3, so Switch 2 is matched + /// by name instead of by enum member - referencing the member directly would stop the SDK from + /// building against the editors that predate it. + /// + internal const string Switch2PlatformName = "Switch2"; + + internal static bool IsSwitch2(this RuntimePlatform platform) => + string.Equals(platform.ToString(), Switch2PlatformName, StringComparison.Ordinal); + internal static bool IsNativeSupportEnabled(this SentryUnityOptions options, RuntimePlatform? platform = null) { platform ??= ApplicationAdapter.Instance.Platform; + + // Switch 2 reuses the Switch native support, and therefore its option. + if (platform.Value.IsSwitch2()) + { + return options.SwitchNativeSupportEnabled; + } + return platform switch { RuntimePlatform.Android => options.AndroidNativeSupportEnabled, diff --git a/test/Sentry.Unity.Tests/SentryUnityOptionsExtensionsTests.cs b/test/Sentry.Unity.Tests/SentryUnityOptionsExtensionsTests.cs index 8ba703b9b..cf802b657 100644 --- a/test/Sentry.Unity.Tests/SentryUnityOptionsExtensionsTests.cs +++ b/test/Sentry.Unity.Tests/SentryUnityOptionsExtensionsTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NUnit.Framework; using Sentry.Unity.Tests.Stubs; @@ -207,4 +208,34 @@ public void IsNativeSupportEnabled_ConsolePlatforms_ReturnsExpectedValue( Assert.AreEqual(expectedResult, result); } + + /// + /// Switch 2 shares the Switch option. It is resolved by name because + /// RuntimePlatform.Switch2 does not exist on the Unity versions the SDK still supports, + /// so this parses the member instead of referencing it and skips where it is unavailable. + /// + [Test] + [TestCase(true, true)] + [TestCase(false, false)] + public void IsNativeSupportEnabled_Switch2_FollowsSwitchOption(bool optionEnabled, bool expectedResult) + { + if (!Enum.TryParse( + SentryUnityOptionsExtensions.Switch2PlatformName, out var switch2)) + { + Assert.Ignore("This Unity version predates 'RuntimePlatform.Switch2'."); + } + + var options = _fixture.GetSut(); + options.SwitchNativeSupportEnabled = optionEnabled; + + Assert.AreEqual(expectedResult, options.IsNativeSupportEnabled(switch2)); + } + + [Test] + public void IsSwitch2_OtherPlatforms_ReturnsFalse() + { + Assert.IsFalse(RuntimePlatform.Switch.IsSwitch2()); + Assert.IsFalse(RuntimePlatform.PS5.IsSwitch2()); + Assert.IsFalse(RuntimePlatform.WindowsPlayer.IsSwitch2()); + } } From b51eb422047622f22bcaf5b7083586ae765728b5 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 4 Sep 2026 12:25:42 +0200 Subject: [PATCH 4/7] fix(switch): ask the platform for network availability instead of reachability Application.internetReachability reports a Switch console as reachable while it is offline - the device logs show "Reachability reported as ReachableViaLocalAreaNetwork" next to every failed send. The reachability gate in the transport was therefore inert on the one platform it was meant to protect, leaving the connection-error backoff to absorb everything. Since attempting the connection is itself what raises the system "connect to the internet" dialog, that still meant a dialog per attempt, just at the backoff's cadence. Let the platform answer instead. sentry-switch now exposes sentry_switch_utils_is_network_available(), which asks the console's network interface manager without submitting a network request, so asking costs nothing and shows no dialog. SentryNativeSwitch hands it to the options as NetworkAvailabilityProbe and the transport prefers it, falling back to reachability where no probe is set - which keeps WebGL and the unknown-platform path behaving as before. The stub returns 1 so builds without the native library keep today's behaviour and rely on the backoff. Requires a sentry-switch build that exports the new function: the P/Invoke goes through __Internal against a statically linked archive, so it resolves at link time rather than falling back at runtime. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- .../Plugins/Switch/sentry_native_stubs.c | 7 ++++ src/Sentry.Unity.Native/SentryNativeSwitch.cs | 18 ++++++++++ src/Sentry.Unity/SentryUnityOptions.cs | 12 +++++++ src/Sentry.Unity/UnityWebRequestTransport.cs | 17 +++++++++- .../UnityWebRequestTransportTests.cs | 34 +++++++++++++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c b/package-dev/Plugins/Switch/sentry_native_stubs.c index 04836e9e0..fcc0e5962 100644 --- a/package-dev/Plugins/Switch/sentry_native_stubs.c +++ b/package-dev/Plugins/Switch/sentry_native_stubs.c @@ -408,6 +408,13 @@ const char* sentry_switch_utils_get_default_user_id(void) return ""; } +int sentry_switch_utils_is_network_available(void) +{ + /* Return 1 - with no native SDK to ask, assume the network is usable and let the + transport fall back on its connection-error backoff */ + return 1; +} + /* * ============================================================================= * Utility Functions diff --git a/src/Sentry.Unity.Native/SentryNativeSwitch.cs b/src/Sentry.Unity.Native/SentryNativeSwitch.cs index 6fab13909..4ca0b243d 100644 --- a/src/Sentry.Unity.Native/SentryNativeSwitch.cs +++ b/src/Sentry.Unity.Native/SentryNativeSwitch.cs @@ -28,8 +28,21 @@ 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; + /// + /// Queries the network interface manager without submitting a network request. + /// + /// + /// Unity's Application.internetReachability reports the console as reachable even while + /// it is offline, and every send attempted in that state raises the system's "connect to the + /// internet" dialog. Asking the native SDK first is what keeps the dialog off the screen. + /// + internal static bool IsNetworkAvailable() => sentry_switch_utils_is_network_available() == 1; + /// /// Configures the native support for Nintendo Switch. /// @@ -72,6 +85,11 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf return; } + // Wired up before the storage and native SDK setup below: the probe only needs the native + // library to be linked, so the transport keeps the benefit even if either of those fails. + 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/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 03af07038..e0574e2b6 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -494,6 +494,18 @@ internal string? DefaultUserId /// internal Action? NativeSupportCloseCallback { get; set; } = null; + /// + /// Reports whether the network is currently usable, when the platform can answer that more + /// reliably than . + /// + /// + /// Set by the platform configuration where a native probe exists. On Nintendo Switch this + /// matters twice over: reachability reports the console as reachable while it is offline, and + /// merely attempting a connection there raises the system's "connect to the internet" dialog. + /// Must not block - it is called on the main thread before each send. + /// + 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 2c502f388..2a27eb02f 100644 --- a/src/Sentry.Unity/UnityWebRequestTransport.cs +++ b/src/Sentry.Unity/UnityWebRequestTransport.cs @@ -109,13 +109,28 @@ internal IEnumerator SendEnvelopeAsync(Envelope envelope) } } + /// + /// Whether the platform reports a usable network. Prefers the platform's own probe where one + /// is available, because Unity's reachability cannot always be trusted - on Nintendo Switch it + /// reports the console as reachable while it is offline. + /// + private bool IsNetworkAvailable() + { + if (_options.NetworkAvailabilityProbe is { } probe) + { + return probe(); + } + + return _application.InternetReachability != NetworkReachability.NotReachable; + } + /// /// Whether it is worth opening a connection for this envelope. Envelopes dropped here are /// recorded as discarded so client reports still account for them. /// private bool CanAttemptSend(Envelope envelope) { - if (_application.InternetReachability == NetworkReachability.NotReachable) + if (!IsNetworkAvailable()) { _options.LogDebug("No network available. Dropping envelope instead of attempting to send."); _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope); diff --git a/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs b/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs index 10cd48b8b..3179edf88 100644 --- a/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs +++ b/test/Sentry.Unity.Tests/UnityWebRequestTransportTests.cs @@ -44,4 +44,38 @@ public void SendEnvelopeAsync_NetworkReachable_AttemptsToSend() Assert.IsTrue(enumerator.MoveNext(), "The transport did not attempt to send while online."); } + + /// + /// The platform probe has the final say. On Nintendo Switch reachability reports the console as + /// reachable while it is offline, and attempting the send is what raises the system dialog - so + /// a probe saying "no network" has to win over reachability saying otherwise. + /// + [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."); + } } From 1ef90654f576d23761a8d1d594fc0a8fb6cf7f14 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 4 Sep 2026 12:36:46 +0200 Subject: [PATCH 5/7] feat(switch2): add Nintendo Switch 2 support Recognise the platform and route it through the existing Switch implementation: UNITY_SWITCH2 defines SENTRY_NATIVE_SWITCH2, which selects SentryNativeSwitch as the platform configuration, and both assembly definitions now include Switch 2 so the runtime code ships to it. Switch 2 gets its own native plugin directory with no-op stubs, matching the Switch layout, so a project without the native library still links. Switch 2 shares the SwitchNativeSupportEnabled option rather than introducing a second one, since it shares the implementation. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- CHANGELOG.md | 6 + .../Plugins/Switch2/sentry_native_stubs.c | 435 ++++++++++++++++++ package-dev/Runtime/SentryInitialization.cs | 8 +- .../io.sentry.unity.dev.runtime.asmdef | 1 + .../Runtime/io.sentry.unity.runtime.asmdef | 1 + .../ConfigurationWindow/AdvancedTab.cs | 2 +- src/Sentry.Unity/Properties/AssemblyInfo.cs | 1 + 7 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 package-dev/Plugins/Switch2/sentry_native_stubs.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2c70910..98e5c6c7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## Unreleased +### Features + +- Added Nintendo Switch 2 support. The SDK now recognises the platform, links the Switch 2 build of the native library from `Assets/Plugins/Sentry/Switch2/`, and uploads its debug symbols. Switch 2 shares the existing `SwitchNativeSupportEnabled` option ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) + ### Fixes - The `UnityWebRequestTransport` no longer opens a connection while the platform reports no network, and backs off exponentially (1s up to 60s) after a connection error instead of retrying on every envelope. On Nintendo Switch each send attempt made while offline raises the system's "connect to the internet" dialog, so a game with logs or metrics enabled produced a prompt every few seconds ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) +- The `UnityLogger` no longer throws while logging an exception whose stack trace cannot be stringified. On Nintendo Switch this escaped from inside the handler that had already dealt with the original error, aborting SDK initialization and leaving the Unity integrations unregistered ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) +- On Nintendo Switch the SDK now asks the native SDK whether the network is usable before sending, instead of relying on `Application.internetReachability`, which reports the console as reachable while it is offline. Every send attempted in that state raised the system's "connect to the internet" dialog. Requires a sentry-switch build exposing `sentry_switch_utils_is_network_available()` ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) ### Dependencies diff --git a/package-dev/Plugins/Switch2/sentry_native_stubs.c b/package-dev/Plugins/Switch2/sentry_native_stubs.c new file mode 100644 index 000000000..fcc0e5962 --- /dev/null +++ b/package-dev/Plugins/Switch2/sentry_native_stubs.c @@ -0,0 +1,435 @@ +/* + * Sentry Switch Stubs + * + * No-op stub implementations for sentry-native and Switch helper functions. + * These stubs are used when the user has not provided the actual sentry-switch + * native library, allowing the SDK to compile and run without native crash support. + * + * When the real sentry-switch library is provided by the user at: + * Assets/Plugins/Sentry/Switch/libsentry.a + * Assets/Plugins/Sentry/Switch/SentrySwitchHelpers.cpp + * + * This stub file will be automatically disabled by the build preprocessor, + * and the real library will be linked instead. + * + * All functions here are no-ops that return safe default values. + * The SDK will appear to initialize successfully, but native features + * (crash reporting, native scope sync) will silently do nothing. + * Managed Sentry features continue to work normally. + */ + +#include +#include +#include + +/* sentry_value_t is an opaque 64-bit union in sentry-native */ +typedef union { + uint64_t _bits; + double _double; +} sentry_value_t; + +/* Null value constant */ +static const sentry_value_t SENTRY_VALUE_NULL = {0}; + +/* + * ============================================================================= + * sentry-native Core Functions + * ============================================================================= + */ + +void* sentry_options_new(void) +{ + /* Return non-null to indicate "success" - value is opaque anyway */ + return (void*)1; +} + +int sentry_init(void* options) +{ + /* Return -1 to indicate failure to let sentry-unity degrade gracefully */ + return -1; +} + +void sentry_close(void) +{ + /* No-op */ +} + +/* + * ============================================================================= + * sentry_options_set_* Functions (No-op) + * ============================================================================= + */ + +void sentry_options_set_dsn(void* options, const char* dsn) +{ + (void)options; + (void)dsn; +} + +void sentry_options_set_release(void* options, const char* release) +{ + (void)options; + (void)release; +} + +void sentry_options_set_environment(void* options, const char* environment) +{ + (void)options; + (void)environment; +} + +void sentry_options_set_debug(void* options, int debug) +{ + (void)options; + (void)debug; +} + +void sentry_options_set_sample_rate(void* options, double rate) +{ + (void)options; + (void)rate; +} + +void sentry_options_set_database_path(void* options, const char* path) +{ + (void)options; + (void)path; +} + +void sentry_options_set_auto_session_tracking(void* options, int track) +{ + (void)options; + (void)track; +} + +void sentry_options_set_attach_screenshot(void* options, int attach) +{ + (void)options; + (void)attach; +} + +void sentry_options_set_enable_metrics(void* options, int enable_metrics) +{ + (void)options; + (void)enable_metrics; +} + +void sentry_options_set_enable_logs(void* options, int enable_logs) +{ + (void)options; + (void)enable_logs; +} + +void sentry_options_set_shutdown_timeout(void* options, uint64_t shutdown_timeout) +{ + (void)options; + (void)shutdown_timeout; +} + +void sentry_options_set_enable_app_hang_tracking(void* options, int enabled) +{ + (void)options; + (void)enabled; +} + +void sentry_options_set_app_hang_timeout(void* options, uint64_t timeout) +{ + (void)options; + (void)timeout; +} + +void sentry_options_set_logger(void* options, void* logger, void* userdata) +{ + (void)options; + (void)logger; + (void)userdata; +} + +void sentry_options_set_logger_enabled_when_crashed(void* options, int enabled) +{ + (void)options; + (void)enabled; +} + +/* + * ============================================================================= + * sentry_value_* Functions + * ============================================================================= + */ + +sentry_value_t sentry_value_new_null(void) +{ + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_bool(int value) +{ + (void)value; + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_int32(int32_t value) +{ + (void)value; + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_double(double value) +{ + (void)value; + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_string(const char* value) +{ + (void)value; + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_object(void) +{ + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_new_breadcrumb(const char* type, const char* message) +{ + (void)type; + (void)message; + return SENTRY_VALUE_NULL; +} + +int sentry_value_set_by_key(sentry_value_t value, const char* k, sentry_value_t v) +{ + (void)value; + (void)k; + (void)v; + return 0; +} + +int sentry_value_is_null(sentry_value_t value) +{ + (void)value; + /* Return 1 (true) - all stub values are effectively null */ + return 1; +} + +int32_t sentry_value_as_int32(sentry_value_t value) +{ + (void)value; + return 0; +} + +double sentry_value_as_double(sentry_value_t value) +{ + (void)value; + return 0.0; +} + +const char* sentry_value_as_string(sentry_value_t value) +{ + (void)value; + return NULL; +} + +size_t sentry_value_get_length(sentry_value_t value) +{ + (void)value; + return 0; +} + +sentry_value_t sentry_value_get_by_index(sentry_value_t value, size_t index) +{ + (void)value; + (void)index; + return SENTRY_VALUE_NULL; +} + +sentry_value_t sentry_value_get_by_key(sentry_value_t value, const char* key) +{ + (void)value; + (void)key; + return SENTRY_VALUE_NULL; +} + +void sentry_value_decref(sentry_value_t value) +{ + (void)value; +} + +/* + * ============================================================================= + * Scope/Context Functions (No-op) + * ============================================================================= + */ + +void sentry_set_context(const char* key, sentry_value_t value) +{ + (void)key; + (void)value; +} + +void sentry_add_breadcrumb(sentry_value_t breadcrumb) +{ + (void)breadcrumb; +} + +void sentry_set_tag(const char* key, const char* value) +{ + (void)key; + (void)value; +} + +void sentry_remove_tag(const char* key) +{ + (void)key; +} + +void sentry_set_user(sentry_value_t user) +{ + (void)user; +} + +void sentry_remove_user(void) +{ + /* No-op */ +} + +void sentry_set_extra(const char* key, sentry_value_t value) +{ + (void)key; + (void)value; +} + +void sentry_remove_extra(const char* key) +{ + (void)key; +} + +void sentry_set_trace(const char* trace_id, const char* parent_span_id) +{ + (void)trace_id; + (void)parent_span_id; +} + +void sentry_set_environment(const char* environment) +{ + (void)environment; +} + +void* sentry_attach_file(const char* path) +{ + (void)path; + return NULL; +} + +void* sentry_attach_bytes(const char* buffer, size_t buffer_length, const char* filename) +{ + (void)buffer; + (void)buffer_length; + (void)filename; + return NULL; +} + +void sentry_clear_attachments(void) +{ + /* No-op */ +} + +/* + * ============================================================================= + * Crash Detection Functions + * ============================================================================= + */ + +int sentry_get_crashed_last_run(void) +{ + /* Return 0 - no crash detected (since we're not tracking) */ + return 0; +} + +int sentry_clear_crashed_last_run(void) +{ + return 0; +} + +void sentry_reinstall_backend(void) +{ + /* No-op */ +} + +void sentry_app_hang_heartbeat(void) +{ + /* No-op */ +} + +void sentry_app_hang_pause(void) +{ + /* No-op */ +} + +sentry_value_t sentry_get_modules_list(void) +{ + /* Return null - no modules to report */ + return SENTRY_VALUE_NULL; +} + +/* + * ============================================================================= + * Switch Helper Functions + * ============================================================================= + */ + +int sentry_switch_utils_mount(void) +{ + /* Return 1 to indicate success - allows SDK initialization to proceed */ + return 1; +} + +const char* sentry_switch_utils_get_cache_path(void) +{ + /* Return a valid-looking path */ + return "sentry:/"; +} + +int sentry_switch_utils_is_mounted(void) +{ + /* Return 1 - pretend we're mounted */ + return 1; +} + +void sentry_switch_utils_unmount(void) +{ + /* No-op */ +} + +const char* sentry_switch_utils_get_default_user_id(void) +{ + /* Return empty string - no user ID available */ + return ""; +} + +int sentry_switch_utils_is_network_available(void) +{ + /* Return 1 - with no native SDK to ask, assume the network is usable and let the + transport fall back on its connection-error backoff */ + return 1; +} + +/* + * ============================================================================= + * Utility Functions + * ============================================================================= + */ + +int vsnprintf_sentry(char* buffer, size_t size, const char* format, va_list args) +{ + (void)format; + (void)args; + + /* Just null-terminate the buffer and return 0 */ + if (buffer != NULL && size > 0) + { + buffer[0] = '\0'; + } + return 0; +} diff --git a/package-dev/Runtime/SentryInitialization.cs b/package-dev/Runtime/SentryInitialization.cs index ddb20b6a0..7f2f84ad3 100644 --- a/package-dev/Runtime/SentryInitialization.cs +++ b/package-dev/Runtime/SentryInitialization.cs @@ -20,6 +20,10 @@ #define SENTRY_NATIVE_SWITCH #endif +#if UNITY_SWITCH2 +#define SENTRY_NATIVE_SWITCH2 +#endif + #if UNITY_WEBGL #define SENTRY_WEBGL #endif @@ -45,7 +49,7 @@ using Sentry.Unity.iOS; #elif SENTRY_NATIVE_ANDROID using Sentry.Unity.Android; -#elif SENTRY_NATIVE || SENTRY_NATIVE_SWITCH +#elif SENTRY_NATIVE || SENTRY_NATIVE_SWITCH || SENTRY_NATIVE_SWITCH2 using Sentry.Unity.Native; #elif SENTRY_WEBGL using Sentry.Unity.WebGL; @@ -108,7 +112,7 @@ private static void SetUpPlatformServices() SentryPlatformServices.PlatformConfiguration = SentryNativeCocoa.Configure; #elif SENTRY_NATIVE_ANDROID SentryPlatformServices.PlatformConfiguration = SentryNativeAndroid.Configure; -#elif SENTRY_NATIVE_SWITCH +#elif SENTRY_NATIVE_SWITCH || SENTRY_NATIVE_SWITCH2 SentryPlatformServices.PlatformConfiguration = SentryNativeSwitch.Configure; #elif SENTRY_NATIVE SentryPlatformServices.PlatformConfiguration = SentryNative.Configure; diff --git a/package-dev/Runtime/io.sentry.unity.dev.runtime.asmdef b/package-dev/Runtime/io.sentry.unity.dev.runtime.asmdef index 357a78477..3f491e257 100644 --- a/package-dev/Runtime/io.sentry.unity.dev.runtime.asmdef +++ b/package-dev/Runtime/io.sentry.unity.dev.runtime.asmdef @@ -9,6 +9,7 @@ "LinuxStandalone64", "macOSStandalone", "Switch", + "Switch2", "PS5", "WSA", "WebGL", diff --git a/package/Runtime/io.sentry.unity.runtime.asmdef b/package/Runtime/io.sentry.unity.runtime.asmdef index e9e1c7df5..38dd0ef4a 100644 --- a/package/Runtime/io.sentry.unity.runtime.asmdef +++ b/package/Runtime/io.sentry.unity.runtime.asmdef @@ -12,6 +12,7 @@ "macOSStandalone", "PS5", "Switch", + "Switch2", "WSA", "WebGL", "WindowsStandalone32", diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs index 241708c97..12d08b08a 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/AdvancedTab.cs @@ -176,7 +176,7 @@ internal static void Display(ScriptableSentryUnityOptions options, SentryCliOpti options.PlayStationNativeSupportEnabled); options.SwitchNativeSupportEnabled = EditorGUILayout.Toggle( - new GUIContent("Nintendo Switch", "Whether to enable native scope sync support on Nintendo Switch."), + new GUIContent("Nintendo Switch", "Whether to enable native scope sync support on Nintendo Switch and Switch 2."), options.SwitchNativeSupportEnabled); } diff --git a/src/Sentry.Unity/Properties/AssemblyInfo.cs b/src/Sentry.Unity/Properties/AssemblyInfo.cs index 9e8226ca7..2254b5e0e 100644 --- a/src/Sentry.Unity/Properties/AssemblyInfo.cs +++ b/src/Sentry.Unity/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ [assembly: InternalsVisibleTo("Sentry.Unity.Native")] [assembly: InternalsVisibleTo("Sentry.Unity.Native.PlayStation")] [assembly: InternalsVisibleTo("Sentry.Unity.Native.Switch")] +[assembly: InternalsVisibleTo("Sentry.Unity.Native.Switch2")] [assembly: InternalsVisibleTo("Sentry.Unity.Native.Xbox")] [assembly: InternalsVisibleTo("Sentry.Unity.Tests")] [assembly: InternalsVisibleTo("Sentry.Unity.Editor")] From a9bc4a41700b579b71da9024935a35e9ca15a95e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 4 Sep 2026 13:01:44 +0200 Subject: [PATCH 6/7] refactor(switch2): serve Switch 2 from the existing Switch stub The Switch 2 stub was a byte-for-byte copy of the Switch one, and it had already drifted: the network availability function added in this branch went into the Switch stub only. Which platforms a stub ships to is decided by its plugin metadata, exactly as it is for the assembly, so one file can serve both. It also could not have worked as it stood. The stub is enabled or disabled at build time depending on whether the user supplied the native libraries, and SwitchNativePluginBuildPreProcess returned early for anything that was not BuildTarget.Switch - so on Switch 2 the stub was never toggled at all, leaving a project without the native libraries unable to link. And because package-dev metadata is gitignored except for named exceptions, the copy's .meta was not tracked, so the duplicate shipped without the import settings that gate it. The preprocessor now runs for both targets, resolves the required libraries per platform, and toggles compatibility for whichever target is being built - the importer tracks that per target, so the two do not interfere. BuildTarget.Switch2 only exists in Unity 6000.3 and newer, so it is matched by name and the value taken from the build report, which keeps this compiling against older editors. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- .../Plugins/Switch/sentry_native_stubs.c.meta | 4 + .../Plugins/Switch2/sentry_native_stubs.c | 435 ------------------ .../SwitchNativePluginBuildPreProcess.cs | 66 ++- 3 files changed, 51 insertions(+), 454 deletions(-) delete mode 100644 package-dev/Plugins/Switch2/sentry_native_stubs.c diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c.meta b/package-dev/Plugins/Switch/sentry_native_stubs.c.meta index 33f21e6e4..c72bd14c2 100644 --- a/package-dev/Plugins/Switch/sentry_native_stubs.c.meta +++ b/package-dev/Plugins/Switch/sentry_native_stubs.c.meta @@ -25,6 +25,7 @@ PluginImporter: Exclude Linux64: 1 Exclude OSXUniversal: 1 Exclude Switch: 0 + Exclude Switch2: 0 Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 @@ -46,6 +47,9 @@ PluginImporter: Switch: enabled: 1 settings: {} + Switch2: + enabled: 1 + settings: {} Win: enabled: 0 settings: diff --git a/package-dev/Plugins/Switch2/sentry_native_stubs.c b/package-dev/Plugins/Switch2/sentry_native_stubs.c deleted file mode 100644 index fcc0e5962..000000000 --- a/package-dev/Plugins/Switch2/sentry_native_stubs.c +++ /dev/null @@ -1,435 +0,0 @@ -/* - * Sentry Switch Stubs - * - * No-op stub implementations for sentry-native and Switch helper functions. - * These stubs are used when the user has not provided the actual sentry-switch - * native library, allowing the SDK to compile and run without native crash support. - * - * When the real sentry-switch library is provided by the user at: - * Assets/Plugins/Sentry/Switch/libsentry.a - * Assets/Plugins/Sentry/Switch/SentrySwitchHelpers.cpp - * - * This stub file will be automatically disabled by the build preprocessor, - * and the real library will be linked instead. - * - * All functions here are no-ops that return safe default values. - * The SDK will appear to initialize successfully, but native features - * (crash reporting, native scope sync) will silently do nothing. - * Managed Sentry features continue to work normally. - */ - -#include -#include -#include - -/* sentry_value_t is an opaque 64-bit union in sentry-native */ -typedef union { - uint64_t _bits; - double _double; -} sentry_value_t; - -/* Null value constant */ -static const sentry_value_t SENTRY_VALUE_NULL = {0}; - -/* - * ============================================================================= - * sentry-native Core Functions - * ============================================================================= - */ - -void* sentry_options_new(void) -{ - /* Return non-null to indicate "success" - value is opaque anyway */ - return (void*)1; -} - -int sentry_init(void* options) -{ - /* Return -1 to indicate failure to let sentry-unity degrade gracefully */ - return -1; -} - -void sentry_close(void) -{ - /* No-op */ -} - -/* - * ============================================================================= - * sentry_options_set_* Functions (No-op) - * ============================================================================= - */ - -void sentry_options_set_dsn(void* options, const char* dsn) -{ - (void)options; - (void)dsn; -} - -void sentry_options_set_release(void* options, const char* release) -{ - (void)options; - (void)release; -} - -void sentry_options_set_environment(void* options, const char* environment) -{ - (void)options; - (void)environment; -} - -void sentry_options_set_debug(void* options, int debug) -{ - (void)options; - (void)debug; -} - -void sentry_options_set_sample_rate(void* options, double rate) -{ - (void)options; - (void)rate; -} - -void sentry_options_set_database_path(void* options, const char* path) -{ - (void)options; - (void)path; -} - -void sentry_options_set_auto_session_tracking(void* options, int track) -{ - (void)options; - (void)track; -} - -void sentry_options_set_attach_screenshot(void* options, int attach) -{ - (void)options; - (void)attach; -} - -void sentry_options_set_enable_metrics(void* options, int enable_metrics) -{ - (void)options; - (void)enable_metrics; -} - -void sentry_options_set_enable_logs(void* options, int enable_logs) -{ - (void)options; - (void)enable_logs; -} - -void sentry_options_set_shutdown_timeout(void* options, uint64_t shutdown_timeout) -{ - (void)options; - (void)shutdown_timeout; -} - -void sentry_options_set_enable_app_hang_tracking(void* options, int enabled) -{ - (void)options; - (void)enabled; -} - -void sentry_options_set_app_hang_timeout(void* options, uint64_t timeout) -{ - (void)options; - (void)timeout; -} - -void sentry_options_set_logger(void* options, void* logger, void* userdata) -{ - (void)options; - (void)logger; - (void)userdata; -} - -void sentry_options_set_logger_enabled_when_crashed(void* options, int enabled) -{ - (void)options; - (void)enabled; -} - -/* - * ============================================================================= - * sentry_value_* Functions - * ============================================================================= - */ - -sentry_value_t sentry_value_new_null(void) -{ - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_bool(int value) -{ - (void)value; - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_int32(int32_t value) -{ - (void)value; - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_double(double value) -{ - (void)value; - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_string(const char* value) -{ - (void)value; - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_object(void) -{ - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_new_breadcrumb(const char* type, const char* message) -{ - (void)type; - (void)message; - return SENTRY_VALUE_NULL; -} - -int sentry_value_set_by_key(sentry_value_t value, const char* k, sentry_value_t v) -{ - (void)value; - (void)k; - (void)v; - return 0; -} - -int sentry_value_is_null(sentry_value_t value) -{ - (void)value; - /* Return 1 (true) - all stub values are effectively null */ - return 1; -} - -int32_t sentry_value_as_int32(sentry_value_t value) -{ - (void)value; - return 0; -} - -double sentry_value_as_double(sentry_value_t value) -{ - (void)value; - return 0.0; -} - -const char* sentry_value_as_string(sentry_value_t value) -{ - (void)value; - return NULL; -} - -size_t sentry_value_get_length(sentry_value_t value) -{ - (void)value; - return 0; -} - -sentry_value_t sentry_value_get_by_index(sentry_value_t value, size_t index) -{ - (void)value; - (void)index; - return SENTRY_VALUE_NULL; -} - -sentry_value_t sentry_value_get_by_key(sentry_value_t value, const char* key) -{ - (void)value; - (void)key; - return SENTRY_VALUE_NULL; -} - -void sentry_value_decref(sentry_value_t value) -{ - (void)value; -} - -/* - * ============================================================================= - * Scope/Context Functions (No-op) - * ============================================================================= - */ - -void sentry_set_context(const char* key, sentry_value_t value) -{ - (void)key; - (void)value; -} - -void sentry_add_breadcrumb(sentry_value_t breadcrumb) -{ - (void)breadcrumb; -} - -void sentry_set_tag(const char* key, const char* value) -{ - (void)key; - (void)value; -} - -void sentry_remove_tag(const char* key) -{ - (void)key; -} - -void sentry_set_user(sentry_value_t user) -{ - (void)user; -} - -void sentry_remove_user(void) -{ - /* No-op */ -} - -void sentry_set_extra(const char* key, sentry_value_t value) -{ - (void)key; - (void)value; -} - -void sentry_remove_extra(const char* key) -{ - (void)key; -} - -void sentry_set_trace(const char* trace_id, const char* parent_span_id) -{ - (void)trace_id; - (void)parent_span_id; -} - -void sentry_set_environment(const char* environment) -{ - (void)environment; -} - -void* sentry_attach_file(const char* path) -{ - (void)path; - return NULL; -} - -void* sentry_attach_bytes(const char* buffer, size_t buffer_length, const char* filename) -{ - (void)buffer; - (void)buffer_length; - (void)filename; - return NULL; -} - -void sentry_clear_attachments(void) -{ - /* No-op */ -} - -/* - * ============================================================================= - * Crash Detection Functions - * ============================================================================= - */ - -int sentry_get_crashed_last_run(void) -{ - /* Return 0 - no crash detected (since we're not tracking) */ - return 0; -} - -int sentry_clear_crashed_last_run(void) -{ - return 0; -} - -void sentry_reinstall_backend(void) -{ - /* No-op */ -} - -void sentry_app_hang_heartbeat(void) -{ - /* No-op */ -} - -void sentry_app_hang_pause(void) -{ - /* No-op */ -} - -sentry_value_t sentry_get_modules_list(void) -{ - /* Return null - no modules to report */ - return SENTRY_VALUE_NULL; -} - -/* - * ============================================================================= - * Switch Helper Functions - * ============================================================================= - */ - -int sentry_switch_utils_mount(void) -{ - /* Return 1 to indicate success - allows SDK initialization to proceed */ - return 1; -} - -const char* sentry_switch_utils_get_cache_path(void) -{ - /* Return a valid-looking path */ - return "sentry:/"; -} - -int sentry_switch_utils_is_mounted(void) -{ - /* Return 1 - pretend we're mounted */ - return 1; -} - -void sentry_switch_utils_unmount(void) -{ - /* No-op */ -} - -const char* sentry_switch_utils_get_default_user_id(void) -{ - /* Return empty string - no user ID available */ - return ""; -} - -int sentry_switch_utils_is_network_available(void) -{ - /* Return 1 - with no native SDK to ask, assume the network is usable and let the - transport fall back on its connection-error backoff */ - return 1; -} - -/* - * ============================================================================= - * Utility Functions - * ============================================================================= - */ - -int vsnprintf_sentry(char* buffer, size_t size, const char* format, va_list args) -{ - (void)format; - (void)args; - - /* Just null-terminate the buffer and return 0 */ - if (buffer != NULL && size > 0) - { - buffer[0] = '\0'; - } - return 0; -} diff --git a/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs b/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs index 104ec7e92..673f6c97f 100644 --- a/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs +++ b/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using Sentry.Extensibility; @@ -21,17 +22,37 @@ namespace Sentry.Unity.Editor.Native; /// internal class SwitchNativePluginBuildPreProcess : IPreprocessBuildWithReport { - private static readonly string[] RequiredFiles = + /// + /// BuildTarget.Switch2 only exists in Unity 6000.3 and newer, and this assembly is + /// compiled against a single Unity version, so the target is matched by name. The build report + /// hands us the value itself, so the enum member never has to be referenced. + /// + internal const string Switch2BuildTargetName = "Switch2"; + + private static bool IsSwitchFamily(BuildTarget target) => + target == BuildTarget.Switch || IsSwitch2(target); + + private static bool IsSwitch2(BuildTarget target) => + string.Equals(target.ToString(), Switch2BuildTargetName, StringComparison.Ordinal); + + /// + /// Both platforms share one stub, so the required libraries are what differ between them. + /// + private static string[] RequiredFilesFor(BuildTarget target) { - "Assets/Plugins/Sentry/Switch/libsentry.a", - "Assets/Plugins/Sentry/Switch/libzstd.a", - }; + var directory = IsSwitch2(target) ? Switch2BuildTargetName : nameof(BuildTarget.Switch); + return new[] + { + $"Assets/Plugins/Sentry/{directory}/libsentry.a", + $"Assets/Plugins/Sentry/{directory}/libzstd.a", + }; + } public int callbackOrder => -100; public void OnPreprocessBuild(BuildReport report) { - if (report.summary.platform != BuildTarget.Switch) + if (!IsSwitchFamily(report.summary.platform)) { return; } @@ -39,14 +60,19 @@ public void OnPreprocessBuild(BuildReport report) var options = SentryScriptableObject.LoadOptions(isBuilding: true); var logger = options?.DiagnosticLogger ?? new UnityLogger(new SentryUnityOptions()); - ConfigureStub(logger, options?.SwitchNativeSupportEnabled ?? false); + // Switch 2 reuses the Switch implementation and therefore its option. + ConfigureStub(logger, options?.SwitchNativeSupportEnabled ?? false, report.summary.platform); } - internal static void ConfigureStub(IDiagnosticLogger logger, bool nativeSupportEnabled) + internal static void ConfigureStub(IDiagnosticLogger logger, bool nativeSupportEnabled, BuildTarget target) { - logger.LogDebug("Switch native support: checking for required files:\n{0}", - string.Join("\n", RequiredFiles.Select(f => $" - {f}"))); + var requiredFiles = RequiredFilesFor(target); + + logger.LogDebug("{0} native support: checking for required files:\n{1}", + target, string.Join("\n", requiredFiles.Select(f => $" - {f}"))); + // One stub serves both platforms; the importer tracks compatibility per build target, so + // enabling it for one does not affect the other. var stubPath = Path.Combine("Packages", SentryPackageInfo.GetName(), "Plugins", "Switch", "sentry_native_stubs.c"); var importer = AssetImporter.GetAtPath(stubPath) as PluginImporter; @@ -56,14 +82,16 @@ internal static void ConfigureStub(IDiagnosticLogger logger, bool nativeSupportE return; } - var existingFiles = RequiredFiles.Where(File.Exists).ToList(); - var missingFiles = RequiredFiles.Except(existingFiles).ToList(); + var existingFiles = requiredFiles.Where(File.Exists).ToList(); + var missingFiles = requiredFiles.Except(existingFiles).ToList(); var someFilesPresent = existingFiles.Count > 0 && missingFiles.Count > 0; if (someFilesPresent) { + // LogError has no two-argument overload that does not also take an exception, so the + // target goes into the format string rather than being passed alongside the file list. logger.LogError( - "Switch native support is partially configured. Missing files:\n{0}\n" + + target + " native support is partially configured. Missing files:\n{0}\n" + "Please add all required files to enable native support, or remove all files to fall back on no-op stubs.\n" + "Build sentry-switch and copy the libraries to the expected locations. " + "See: https://github.com/getsentry/sentry-switch", @@ -75,26 +103,26 @@ internal static void ConfigureStub(IDiagnosticLogger logger, bool nativeSupportE var allFilesPresent = missingFiles.Count == 0; if (allFilesPresent) { - logger.LogInfo("Switch native libraries found:\n{0}", - string.Join("\n", existingFiles.Select(f => $" - {f}"))); - importer.SetCompatibleWithPlatform(BuildTarget.Switch, false); + logger.LogInfo("{0} native libraries found:\n{1}", + target, string.Join("\n", existingFiles.Select(f => $" - {f}"))); + importer.SetCompatibleWithPlatform(target, false); } else { if (nativeSupportEnabled) { logger.LogWarning( - "Switch native support is enabled but required files are missing:\n{0}\n" + + "{0} native support is enabled but required files are missing:\n{1}\n" + "Build sentry-switch and copy the libraries to the expected locations. " + "See: https://github.com/getsentry/sentry-switch", - string.Join("\n", missingFiles.Select(f => $" - {f}")) + target, string.Join("\n", missingFiles.Select(f => $" - {f}")) ); } else { - logger.LogDebug("Switch native support is disabled. Enabling stubs (native calls will be no-op)."); + logger.LogDebug("{0} native support is disabled. Enabling stubs (native calls will be no-op).", target); } - importer.SetCompatibleWithPlatform(BuildTarget.Switch, true); + importer.SetCompatibleWithPlatform(target, true); } importer.SaveAndReimport(); From a890be4801f0d6bcd796c3ec4dd9d4a5c842a59f Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 4 Sep 2026 13:06:34 +0200 Subject: [PATCH 7/7] chore: reference the pull request in the changelog entries Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ABHTqM37qGAc9iQZcdbdY8 --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e5c6c7f..770fb4b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,13 @@ ### Features -- Added Nintendo Switch 2 support. The SDK now recognises the platform, links the Switch 2 build of the native library from `Assets/Plugins/Sentry/Switch2/`, and uploads its debug symbols. Switch 2 shares the existing `SwitchNativeSupportEnabled` option ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) +- Added Nintendo Switch 2 support. The SDK now recognises the platform, links the Switch 2 build of the native library from `Assets/Plugins/Sentry/Switch2/`, and uploads its debug symbols. Switch 2 shares the existing `SwitchNativeSupportEnabled` option ([#2831](https://github.com/getsentry/sentry-unity/pull/2831)) ### Fixes -- The `UnityWebRequestTransport` no longer opens a connection while the platform reports no network, and backs off exponentially (1s up to 60s) after a connection error instead of retrying on every envelope. On Nintendo Switch each send attempt made while offline raises the system's "connect to the internet" dialog, so a game with logs or metrics enabled produced a prompt every few seconds ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) -- The `UnityLogger` no longer throws while logging an exception whose stack trace cannot be stringified. On Nintendo Switch this escaped from inside the handler that had already dealt with the original error, aborting SDK initialization and leaving the Unity integrations unregistered ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) -- On Nintendo Switch the SDK now asks the native SDK whether the network is usable before sending, instead of relying on `Application.internetReachability`, which reports the console as reachable while it is offline. Every send attempted in that state raised the system's "connect to the internet" dialog. Requires a sentry-switch build exposing `sentry_switch_utils_is_network_available()` ([#TBD](https://github.com/getsentry/sentry-unity/pull/TBD)) +- The `UnityWebRequestTransport` no longer opens a connection while the platform reports no network, and backs off exponentially (1s up to 60s) after a connection error instead of retrying on every envelope. On Nintendo Switch each send attempt made while offline raises the system's "connect to the internet" dialog, so a game with logs or metrics enabled produced a prompt every few seconds ([#2831](https://github.com/getsentry/sentry-unity/pull/2831)) +- The `UnityLogger` no longer throws while logging an exception whose stack trace cannot be stringified. On Nintendo Switch this escaped from inside the handler that had already dealt with the original error, aborting SDK initialization and leaving the Unity integrations unregistered ([#2831](https://github.com/getsentry/sentry-unity/pull/2831)) +- On Nintendo Switch the SDK now asks the native SDK whether the network is usable before sending, instead of relying on `Application.internetReachability`, which reports the console as reachable while it is offline. Every send attempted in that state raised the system's "connect to the internet" dialog. Requires a sentry-switch build exposing `sentry_switch_utils_is_network_available()` ([#2831](https://github.com/getsentry/sentry-unity/pull/2831)) ### Dependencies