From d97b4cb9aa49698237886d00cadfbd02d9572e6b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 19 Jun 2025 14:47:43 +1200 Subject: [PATCH 01/12] fix: Source context for class libraries when running on Android in Release mode Resolves: #4278 - https://github.com/getsentry/sentry-dotnet/issues/4278 --- .../V2/AndroidAssemblyStoreReaderV2.cs | 13 +++++++++++++ .../V2/AssemblyStoreExplorer.cs | 1 + 2 files changed, 14 insertions(+) diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs index f88e0ca237..08531fc37e 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs @@ -100,11 +100,22 @@ public static bool TryReadStore(string inputFile, IList supportedAbis, D return assembly; } + // If the assembly name ends with .dll or .exe, try to find it without the extension. if ((IsFileType(".dll") || IsFileType(".exe")) && FindBestAssembly(name[..^4], out assembly)) { return assembly; } + // Conversely, if there is no extension, try to find it with one of those extensions + // See: https://github.com/getsentry/sentry-dotnet/issues/4278#issuecomment-2986009125 + if (!(IsFileType(".dll") && !IsFileType(".exe"))) + { + if (FindBestAssembly(name + ".dll", out assembly) || FindBestAssembly(name + ".exe", out assembly)) + { + return assembly; + } + } + return null; bool IsFileType(string extension) @@ -119,10 +130,12 @@ private bool FindBestAssembly(string name, out ExplorerStoreItem? explorerAssemb { if (explorer.AssembliesByName?.TryGetValue(name, out var assembly) is true) { + _logger?.Invoke("Found best assembly {0} in APK AssemblyStore for target arch {1}", name, explorer.TargetArch); explorerAssembly = new(explorer, assembly); return true; } } + _logger?.Invoke("No best assembly for {0} in APK AssemblyStore", name); explorerAssembly = null; return false; } diff --git a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs index de2148ddeb..424109bcee 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AssemblyStoreExplorer.cs @@ -33,6 +33,7 @@ private AssemblyStoreExplorer(Stream storeStream, string path, DebugLogger? logg { foreach (var item in Assemblies) { + logger?.Invoke("Assembly {0} indexed from AssemblyStore {1}", item.Name, path); dict.Add(item.Name, item); } } From 99ecda79244acce251bf9f4ce4a0db585a38a8fa Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 19 Jun 2025 17:00:15 +1200 Subject: [PATCH 02/12] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28091a8ff7..7bfc22cd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - ExtraData not captured for Breadcrumbs in MauiEventsBinder ([#4254](https://github.com/getsentry/sentry-dotnet/pull/4254)) - NOTE: Required breaking changes to the public API of `Sentry.Maui.BreadcrumbEvent`, while keeping an _Obsolete_ constructor for backward compatibility. - InvalidOperationException sending attachments on Android with LLVM enabled ([#4276](https://github.com/getsentry/sentry-dotnet/pull/4276)) +- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294)) ### Dependencies From 0fae5db1d0adf5e7e4aa80aca987809330696f23 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 8 Jul 2025 16:13:20 +1200 Subject: [PATCH 03/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4e8f3f1b..05e4960aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318)) - Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314)) +- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294)) ### Dependencies @@ -47,7 +48,6 @@ - ExtraData not captured for Breadcrumbs in MauiEventsBinder ([#4254](https://github.com/getsentry/sentry-dotnet/pull/4254)) - NOTE: Required breaking changes to the public API of `Sentry.Maui.BreadcrumbEvent`, while keeping an _Obsolete_ constructor for backward compatibility. - InvalidOperationException sending attachments on Android with LLVM enabled ([#4276](https://github.com/getsentry/sentry-dotnet/pull/4276)) -- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294)) - When CaptureFeedback methods are called with invalid email addresses, the email address will be removed and, if Debug mode is enabled, a warning will be logged. This is done to avoid losing the Feedback altogether (Sentry would reject Feedback that has an invalid email address) ([#4284](https://github.com/getsentry/sentry-dotnet/pull/4284)) ### Dependencies From be7ea6e48be522aa8a4062d23f44207c6594c88a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 9 Jul 2025 10:38:15 +1200 Subject: [PATCH 04/12] Update AndroidAssemblyStoreReaderV2.cs --- .../V2/AndroidAssemblyStoreReaderV2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs index 08531fc37e..d4b8caea22 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs @@ -106,11 +106,11 @@ public static bool TryReadStore(string inputFile, IList supportedAbis, D return assembly; } - // Conversely, if there is no extension, try to find it with one of those extensions + // Conversely, if there is no extension, try with the dll extension (sometimes required for class libraries). // See: https://github.com/getsentry/sentry-dotnet/issues/4278#issuecomment-2986009125 if (!(IsFileType(".dll") && !IsFileType(".exe"))) { - if (FindBestAssembly(name + ".dll", out assembly) || FindBestAssembly(name + ".exe", out assembly)) + if (FindBestAssembly(name + ".dll", out assembly)) { return assembly; } From 057489f299662c2c6d0f585b7161f49b88442d5b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 9 Jul 2025 10:40:48 +1200 Subject: [PATCH 05/12] Update AndroidAssemblyStoreReaderV2.cs --- .../V2/AndroidAssemblyStoreReaderV2.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs index d4b8caea22..e49fbf7147 100644 --- a/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs +++ b/src/Sentry.Android.AssemblyReader/V2/AndroidAssemblyStoreReaderV2.cs @@ -108,12 +108,9 @@ public static bool TryReadStore(string inputFile, IList supportedAbis, D // Conversely, if there is no extension, try with the dll extension (sometimes required for class libraries). // See: https://github.com/getsentry/sentry-dotnet/issues/4278#issuecomment-2986009125 - if (!(IsFileType(".dll") && !IsFileType(".exe"))) + if (!IsFileType(".dll") && !IsFileType(".exe") && FindBestAssembly(name + ".dll", out assembly)) { - if (FindBestAssembly(name + ".dll", out assembly)) - { - return assembly; - } + return assembly; } return null; From 19f22eca1008d16565e96c2b3e636736bc558af4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 9 Jul 2025 15:27:40 +1200 Subject: [PATCH 06/12] Update CachingTransportTests.cs --- .../Internals/Http/CachingTransportTests.cs | 295 +++++------------- 1 file changed, 82 insertions(+), 213 deletions(-) diff --git a/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs b/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs index e903b7f3ca..8f5d821a1c 100644 --- a/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/CachingTransportTests.cs @@ -3,31 +3,38 @@ namespace Sentry.Tests.Internals.Http; -public class CachingTransportTests +public class CachingTransportTests : IDisposable { private readonly TestOutputDiagnosticLogger _logger; + private readonly TempDirectory _cacheDirectory; + private readonly SentryOptions _options; public CachingTransportTests(ITestOutputHelper testOutputHelper) { _logger = Substitute.ForPartsOf(testOutputHelper); - } - - [Fact] - public async Task WithAttachment() - { - // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions + _cacheDirectory = new TempDirectory(); + _options = new SentryOptions { Dsn = ValidDsn, DiagnosticLogger = _logger, Debug = true, - CacheDirectoryPath = cacheDirectory.Path + CacheDirectoryPath = _cacheDirectory.Path, + NetworkStatusListener = FakeReliableNetworkStatusListener.Instance }; + } + + public void Dispose() + { + _cacheDirectory.Dispose(); + } + [Fact] + public async Task WithAttachment() + { + // Arrange string httpContent = null; Exception exception = null; - var innerTransport = new HttpTransport(options, new HttpClient(new CallbackHttpClientHandler(async message => + var innerTransport = new HttpTransport(_options, new HttpClient(new CallbackHttpClientHandler(async message => { try { @@ -39,7 +46,7 @@ public async Task WithAttachment() } }))); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); const string attachmentContent = "test-attachment"; var tempFile = Path.GetTempFileName(); @@ -76,17 +83,8 @@ public async Task WithAttachment() public async Task WorksInBackground() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options); + await using var transport = CachingTransport.Create(innerTransport, _options); // Attach to the EnvelopeSent event. We'll wait for that below. // ReSharper disable once AccessToDisposedClosure @@ -108,16 +106,6 @@ public async Task WorksInBackground() public async Task ShouldNotLogOperationCanceledExceptionWhenIsCancellationRequested() { // Arrange - using var cacheDirectory = new TempDirectory(); - - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - CacheDirectoryPath = cacheDirectory.Path, - Debug = true - }; - var capturingCompletionSource = new TaskCompletionSource(); var cancelingCompletionSource = new TaskCompletionSource(); @@ -132,7 +120,7 @@ public async Task ShouldNotLogOperationCanceledExceptionWhenIsCancellationReques throw new OperationCanceledException(); }); - await using var transport = CachingTransport.Create(innerTransport, options); + await using var transport = CachingTransport.Create(innerTransport, _options); using var envelope = Envelope.FromEvent(new SentryEvent()); await transport.SendEnvelopeAsync(envelope); @@ -151,7 +139,6 @@ public async Task ShouldNotLogOperationCanceledExceptionWhenIsCancellationReques public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationRequested() { // Arrange - using var cacheDirectory = new TempDirectory(); var loggerCompletionSource = new TaskCompletionSource(); _logger @@ -162,14 +149,6 @@ public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationReques Arg.Any())) .Do(_ => loggerCompletionSource.SetResult(null)); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - CacheDirectoryPath = cacheDirectory.Path, - Debug = true - }; - var innerTransport = Substitute.For(); var capturingCompletionSource = new TaskCompletionSource(); @@ -181,7 +160,7 @@ public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationReques return new OperationCanceledException(); }); - await using var transport = CachingTransport.Create(innerTransport, options); + await using var transport = CachingTransport.Create(innerTransport, _options); using var envelope = Envelope.FromEvent(new SentryEvent()); await transport.SendEnvelopeAsync(envelope); @@ -196,17 +175,8 @@ public async Task ShouldLogOperationCanceledExceptionWhenNotIsCancellationReques public async Task EnvelopeReachesInnerTransport() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -222,46 +192,27 @@ public async Task EnvelopeReachesInnerTransport() public async Task MaintainsLimit() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - MaxCacheItems = 2 - }; - var innerTransport = Substitute.For(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act - for (var i = 0; i < options.MaxCacheItems + 2; i++) + for (var i = 0; i < _options.MaxCacheItems + 2; i++) { using var envelope = Envelope.FromEvent(new SentryEvent()); await transport.SendEnvelopeAsync(envelope); } // Assert - transport.GetCacheLength().Should().BeLessOrEqualTo(options.MaxCacheItems); + transport.GetCacheLength().Should().BeLessOrEqualTo(_options.MaxCacheItems); } [Fact] public async Task AwareOfExistingFiles() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - // Send some envelopes with a failing transport to make sure they all stay in cache var initialInnerTransport = new FakeFailingTransport(); - await using var initialTransport = CachingTransport.Create(initialInnerTransport, options, startWorker: false); + await using var initialTransport = CachingTransport.Create(initialInnerTransport, _options, startWorker: false); for (var i = 0; i < 3; i++) { @@ -276,7 +227,7 @@ public async Task AwareOfExistingFiles() // Creating the transport should move files from processing during initialization. using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Flushing the worker will ensure all files are processed. await transport.FlushAsync(); @@ -289,55 +240,38 @@ public async Task AwareOfExistingFiles() public async Task Handle_Malformed_Envelopes_Gracefully() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - }; var cacheDirectoryPath = - options.TryGetProcessSpecificCacheDirectoryPath() ?? + _options.TryGetProcessSpecificCacheDirectoryPath() ?? throw new InvalidOperationException("Cache directory or DSN is not set."); var processingDirectoryPath = Path.Combine(cacheDirectoryPath, "__processing"); var fileName = $"{Guid.NewGuid()}.envelope"; var filePath = Path.Combine(processingDirectoryPath, fileName); - options.FileSystem.CreateDirectory(processingDirectoryPath); // Create the processing directory - options.FileSystem.CreateFileForWriting(filePath, out var file); + _options.FileSystem.CreateDirectory(processingDirectoryPath); // Create the processing directory + _options.FileSystem.CreateFileForWriting(filePath, out var file); file.Dispose(); // Make a malformed envelope... just an empty file - options.FileSystem.FileExists(filePath).Should().BeTrue(); + _options.FileSystem.FileExists(filePath).Should().BeTrue(); // Act using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); await transport.FlushAsync(); // Flush the worker to process // Assert - options.FileSystem.FileExists(filePath).Should().BeFalse(); + _options.FileSystem.FileExists(filePath).Should().BeFalse(); } [Fact] public async Task NonTransientExceptionShouldLog() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - var innerTransport = Substitute.For(); innerTransport .SendEnvelopeAsync(Arg.Any(), Arg.Any()) .Returns(_ => Task.FromException(new Exception("The Message"))); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -358,15 +292,6 @@ public async Task NonTransientExceptionShouldLog() public async Task DoesNotRetryOnNonTransientExceptions() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - var innerTransport = Substitute.For(); var isFailing = true; @@ -378,7 +303,7 @@ public async Task DoesNotRetryOnNonTransientExceptions() ? Task.FromException(new InvalidOperationException()) : Task.CompletedTask); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act for (var i = 0; i < 3; i++) @@ -403,15 +328,7 @@ public async Task DoesNotRetryOnNonTransientExceptions() public async Task RecordsDiscardedEventOnNonTransientExceptions() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - ClientReportRecorder = Substitute.For() - }; + _options.ClientReportRecorder = Substitute.For(); var innerTransport = Substitute.For(); @@ -419,7 +336,7 @@ public async Task RecordsDiscardedEventOnNonTransientExceptions() .SendEnvelopeAsync(Arg.Any(), Arg.Any()) .Returns(_ => Task.FromException(new InvalidOperationException())); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -427,7 +344,7 @@ public async Task RecordsDiscardedEventOnNonTransientExceptions() await transport.FlushAsync(); // Test that we recorded the discarded event - options.ClientReportRecorder.Received(1) + _options.ClientReportRecorder.Received(1) .RecordDiscardedEvent(DiscardReason.CacheOverflow, DataCategory.Error); } @@ -435,22 +352,13 @@ public async Task RecordsDiscardedEventOnNonTransientExceptions() public async Task RoundtripsClientReports() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - var timestamp = DateTimeOffset.UtcNow; var clock = new MockClock(timestamp); - var recorder = new ClientReportRecorder(options, clock); - options.ClientReportRecorder = recorder; + var recorder = new ClientReportRecorder(_options, clock); + _options.ClientReportRecorder = recorder; var innerTransport = new FakeTransportWithRecorder(recorder); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act recorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Error); @@ -481,18 +389,9 @@ public async Task RoundtripsClientReports() public async Task RestoresDiscardedEventCounts() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path - }; - - var recorder = (ClientReportRecorder)options.ClientReportRecorder; + var recorder = (ClientReportRecorder)_options.ClientReportRecorder; var innerTransport = new FakeTransportWithRecorder(recorder); - await using var transport = CachingTransport.Create(innerTransport, options, + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false, failStorage: true); // some arbitrary discarded events ahead of time @@ -534,17 +433,9 @@ await Assert.ThrowsAnyAsync(async () => public async Task TestNetworkException(Exception exception) { // Arrange - network unavailable - using var cacheDirectory = new TempDirectory(); var pingHost = Substitute.For(); pingHost.IsAvailableAsync(Arg.Any()).Returns(Task.FromResult(true)); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = new PollingNetworkStatusListener(pingHost) - }; + _options.NetworkStatusListener = new PollingNetworkStatusListener(pingHost); var receivedException = new Exception(); var innerTransport = Substitute.For(); @@ -553,7 +444,7 @@ public async Task TestNetworkException(Exception exception) .SendEnvelopeAsync(Arg.Any(), Arg.Any()) .Returns(_ => Task.FromException(exception)); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); using var envelope = Envelope.FromEvent(new SentryEvent()); await transport.SendEnvelopeAsync(envelope); @@ -570,7 +461,7 @@ public async Task TestNetworkException(Exception exception) // Assert receivedException.Should().Be(exception); - var files = options.FileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); + var files = _options.FileSystem.EnumerateFiles(_options.CacheDirectoryPath!, "*", SearchOption.AllDirectories).ToArray(); files.Should().NotBeEmpty(); // Arrange - network recovery @@ -584,7 +475,7 @@ public async Task TestNetworkException(Exception exception) // Assert receivedException.Should().Be(exception); - files = options.FileSystem.EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories).ToArray(); + files = _options.FileSystem.EnumerateFiles(_options.CacheDirectoryPath, "*", SearchOption.AllDirectories).ToArray(); files.Should().NotContain(file => file.Contains("__processing", StringComparison.OrdinalIgnoreCase)); } @@ -597,18 +488,10 @@ public async Task TransportSendsWithoutWaitingWhenNetworkIsOnline() listener.WaitForNetworkOnlineAsync(Arg.Any()) .Throws(new Exception("We should not be waiting for the network status if we know it's online.")); - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = listener - }; + _options.NetworkStatusListener = listener; using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -634,18 +517,10 @@ public async Task TransportPausesWhenNetworkIsOffline() return Task.Delay(Timeout.Infinite, callInfo.Arg()); }); - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = listener - }; + _options.NetworkStatusListener = listener; using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); using var cts = new CancellationTokenSource(); try { @@ -688,18 +563,10 @@ public async Task TransportResumesWhenNetworkComesBackOnline() return Task.CompletedTask; }); - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = listener - }; + _options.NetworkStatusListener = listener; using var innerTransport = new FakeTransport(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -718,23 +585,15 @@ public async Task FlushAsync_RejectedByServer_DiscardsEnvelope() var listener = Substitute.For(); listener.Online.Returns(_ => true); - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - NetworkStatusListener = listener, - ClientReportRecorder = Substitute.For() - }; + _options.NetworkStatusListener = listener; + _options.ClientReportRecorder = Substitute.For(); using var envelope = Envelope.FromEvent(new SentryEvent()); var innerTransport = Substitute.For(); innerTransport.SendEnvelopeAsync(Arg.Any(), Arg.Any()) .Returns(_ => throw new SocketException(32 /* Bad pipe exception */)); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); // Act await transport.SendEnvelopeAsync(envelope); @@ -743,7 +602,7 @@ public async Task FlushAsync_RejectedByServer_DiscardsEnvelope() // Assert foreach (var item in envelope.Items) { - options.ClientReportRecorder.Received(1) + _options.ClientReportRecorder.Received(1) .RecordDiscardedEvent(DiscardReason.BufferOverflow, item.DataCategory); } } @@ -752,19 +611,10 @@ public async Task FlushAsync_RejectedByServer_DiscardsEnvelope() public async Task DoesntWriteSentAtHeaderToCacheFile() { // Arrange - using var cacheDirectory = new TempDirectory(); - var options = new SentryOptions - { - Dsn = ValidDsn, - DiagnosticLogger = _logger, - Debug = true, - CacheDirectoryPath = cacheDirectory.Path, - // This keeps all writing-to-file operations in memory instead of actually writing to disk - FileSystem = new FakeFileSystem() - }; + _options.FileSystem = new FakeFileSystem(); // Keeps file write operations in memory instead of writing to disk var innerTransport = Substitute.For(); - await using var transport = CachingTransport.Create(innerTransport, options, startWorker: false); + await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false); using var envelope = Envelope.FromEvent(new SentryEvent()); @@ -772,11 +622,30 @@ public async Task DoesntWriteSentAtHeaderToCacheFile() await transport.SendEnvelopeAsync(envelope); // Assert - var filePath = options.FileSystem - .EnumerateFiles(cacheDirectory.Path, "*", SearchOption.AllDirectories) + var filePath = _options.FileSystem + .EnumerateFiles(_options.CacheDirectoryPath!, "*", SearchOption.AllDirectories) .Single(); - var contents = options.FileSystem.ReadAllTextFromFile(filePath); + var contents = _options.FileSystem.ReadAllTextFromFile(filePath); Assert.DoesNotContain("sent_at", contents); } } + +/// +/// We don't want the real network status to affect the reliability of our tests +/// +file class FakeReliableNetworkStatusListener : INetworkStatusListener +{ + public static readonly FakeReliableNetworkStatusListener Instance = new(); + + private FakeReliableNetworkStatusListener() + { + } + + public bool Online => true; + + public Task WaitForNetworkOnlineAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} From 544b3548230b0e67559348a4ef66b26b4a48fb4b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 14 Jul 2025 11:03:41 +1200 Subject: [PATCH 07/12] Update Sentry.Maui.Device.TestApp.csproj --- .../Sentry.Maui.Device.TestApp.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj index d6d124e287..15beeb7e92 100644 --- a/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj +++ b/test/Sentry.Maui.Device.TestApp/Sentry.Maui.Device.TestApp.csproj @@ -100,6 +100,9 @@ + From 6dd521851e9fdb2c62ac6532b5419d963f6886a3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 14 Jul 2025 17:24:21 +1200 Subject: [PATCH 08/12] Update device-tests-android.yml --- .github/workflows/device-tests-android.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/device-tests-android.yml b/.github/workflows/device-tests-android.yml index 953dab0a49..9da13e57be 100644 --- a/.github/workflows/device-tests-android.yml +++ b/.github/workflows/device-tests-android.yml @@ -59,7 +59,8 @@ jobs: matrix: tfm: [net9.0] # We run against both an older and a newer API - api-level: [27, 33] + # targetSdk must be 34+ for new apps and app updates as of August 31, 2024. + api-level: [34, 36] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: 1 From ce0a859c19c5db65f63411cb67849a35bee8934a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 21:24:06 +1200 Subject: [PATCH 09/12] Exclude SentryMauiOptionsTests (testing) --- test/Sentry.Maui.Device.TestApp/Startup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Sentry.Maui.Device.TestApp/Startup.cs b/test/Sentry.Maui.Device.TestApp/Startup.cs index 9f3e614d9c..ad93281b3c 100644 --- a/test/Sentry.Maui.Device.TestApp/Startup.cs +++ b/test/Sentry.Maui.Device.TestApp/Startup.cs @@ -14,7 +14,7 @@ public static MauiApp CreateMauiApp() [ typeof(Sentry.Tests.SentrySdkTests).Assembly, typeof(Sentry.Extensions.Logging.Tests.LogLevelExtensionsTests).Assembly, - typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly, + // typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly, #if NET9_0_OR_GREATER typeof(Sentry.Maui.CommunityToolkit.Mvvm.Tests.MauiCommunityToolkitMvvmEventsBinderTests).Assembly, #endif From a33c654626534aae2cbeb889d68a5b6623c6d6df Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 21:25:00 +1200 Subject: [PATCH 10/12] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 064a569bab..ecb95f1893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294)) + ### Dependencies - Bump CLI from v2.47.0 to v2.47.1 ([#4348](https://github.com/getsentry/sentry-dotnet/pull/4348)) @@ -24,7 +28,6 @@ - Sentry now decompresses Request bodies in ASP.NET Core when RequestDecompression middleware is enabled ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) - Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318)) - Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314)) -- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294)) - Fixed an issue with the way Sentry detects build settings. This was causing Sentry to produce code that could fail at runtime in AOT compiled applications. ([#4333](https://github.com/getsentry/sentry-dotnet/pull/4333)) - Native AOT: link to static `lzma` on Linux/MUSL ([#4326](https://github.com/getsentry/sentry-dotnet/pull/4326)) - AppDomain.CurrentDomain.ProcessExit hook is now removed on shutdown ([#4323](https://github.com/getsentry/sentry-dotnet/pull/4323)) From 2b15fda3a6efbba0de5fbcc3ab3832c09c4877d2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 21:52:29 +1200 Subject: [PATCH 11/12] Try FakeReliableNetworkStatusListener in SentryMauiOptionsTests --- test/Sentry.Maui.Device.TestApp/Startup.cs | 2 +- .../SentryMauiOptionsTests.cs | 25 +++++++++++-------- .../FakeReliableNetworkStatusListener.cs | 20 +++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 test/Sentry.Testing/FakeReliableNetworkStatusListener.cs diff --git a/test/Sentry.Maui.Device.TestApp/Startup.cs b/test/Sentry.Maui.Device.TestApp/Startup.cs index ad93281b3c..9f3e614d9c 100644 --- a/test/Sentry.Maui.Device.TestApp/Startup.cs +++ b/test/Sentry.Maui.Device.TestApp/Startup.cs @@ -14,7 +14,7 @@ public static MauiApp CreateMauiApp() [ typeof(Sentry.Tests.SentrySdkTests).Assembly, typeof(Sentry.Extensions.Logging.Tests.LogLevelExtensionsTests).Assembly, - // typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly, + typeof(Sentry.Maui.Tests.SentryMauiOptionsTests).Assembly, #if NET9_0_OR_GREATER typeof(Sentry.Maui.CommunityToolkit.Mvvm.Tests.MauiCommunityToolkitMvvmEventsBinderTests).Assembly, #endif diff --git a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs index 55d1ab909a..ddb28f3bf7 100644 --- a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs +++ b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs @@ -4,45 +4,50 @@ namespace Sentry.Maui.Tests; public class SentryMauiOptionsTests { + private static SentryMauiOptions GetSut() => new() + { + NetworkStatusListener = FakeReliableNetworkStatusListener.Instance + }; + [Fact] public void IncludeTextInBreadcrumbs_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); Assert.False(options.IncludeTextInBreadcrumbs); } [Fact] public void IncludeTitleInBreadcrumbs_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); Assert.False(options.IncludeTitleInBreadcrumbs); } [Fact] public void IncludeBackgroundingStateInBreadcrumbs_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); Assert.False(options.IncludeBackgroundingStateInBreadcrumbs); } [Fact] public void AutoSessionTracking_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); Assert.True(options.AutoSessionTracking); } [Fact] public void DetectStartupTime_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); Assert.Equal(StartupTimeDetectionMode.Fast, options.DetectStartupTime); } [Fact] public void CacheDirectoryPath_Default() { - var options = new SentryMauiOptions(); + var options = GetSut(); #if PLATFORM_NEUTRAL Assert.Null(options.CacheDirectoryPath); @@ -58,7 +63,7 @@ public void HandlerStrategy_Default() { // Arrange var expected = Android.LogCatIntegrationType.None; - var options = new SentryMauiOptions(); + var options = GetSut(); // Assert Assert.Equal(expected, options.Android.LogCatIntegration); @@ -69,7 +74,7 @@ public void HandlerStrategy_Set() { // Arrange var expected = Android.LogCatIntegrationType.None; - var options = new SentryMauiOptions(); + var options = GetSut(); // Act options.Android.LogCatIntegration = Android.LogCatIntegrationType.All; @@ -83,7 +88,7 @@ public void HandlerStrategy_Set() public void BeforeCaptureScreenshot_Set() { // Arrange - var options = new SentryMauiOptions(); + var options = GetSut(); options.AttachScreenshot = true; // Act @@ -101,7 +106,7 @@ public void BeforeCaptureScreenshot_Set() public void BeforeCaptureScreenshot_NotSet() { // Arrange - var options = new SentryMauiOptions(); + var options = GetSut(); options.AttachScreenshot = true; // Assert diff --git a/test/Sentry.Testing/FakeReliableNetworkStatusListener.cs b/test/Sentry.Testing/FakeReliableNetworkStatusListener.cs new file mode 100644 index 0000000000..b239439597 --- /dev/null +++ b/test/Sentry.Testing/FakeReliableNetworkStatusListener.cs @@ -0,0 +1,20 @@ +namespace Sentry.Testing; + +/// +/// To be used when we don't want the real network status to affect the reliability of our tests +/// +public class FakeReliableNetworkStatusListener : INetworkStatusListener +{ + public static readonly FakeReliableNetworkStatusListener Instance = new(); + + private FakeReliableNetworkStatusListener() + { + } + + public bool Online => true; + + public Task WaitForNetworkOnlineAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} From 982ffe48588e968fbd0312522b4d7291b4862350 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 22:28:22 +1200 Subject: [PATCH 12/12] Disable all but one SentryMauiOptionsTests --- test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs index ddb28f3bf7..0585fd9dd9 100644 --- a/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs +++ b/test/Sentry.Maui.Tests/SentryMauiOptionsTests.cs @@ -16,6 +16,7 @@ public void IncludeTextInBreadcrumbs_Default() Assert.False(options.IncludeTextInBreadcrumbs); } +#if false [Fact] public void IncludeTitleInBreadcrumbs_Default() { @@ -99,7 +100,6 @@ public void BeforeCaptureScreenshot_Set() // Assert Assert.NotNull(options.BeforeCaptureInternal); - } [Fact] @@ -111,6 +111,6 @@ public void BeforeCaptureScreenshot_NotSet() // Assert Assert.Null(options.BeforeCaptureInternal); - } +#endif }