Skip to content

Commit da54825

Browse files
committed
Improve sharing failure diagnostics
Log handled publishing and clipboard failures with their full exception chains so recoverable sharing issues can be diagnosed without interrupting users.\n\nKeep the share dialog out of the taskbar and cover operation-failure logging, including nested transport exceptions.
1 parent 45a76fd commit da54825

5 files changed

Lines changed: 79 additions & 7 deletions

File tree

‎src/NoteManager.Core/Services/ApplicationActivityLog.cs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace NoteManager.App.Services;
44

55
/// <summary>
6-
/// Writes a small local audit trail for application startup, folder use, and crashes.
6+
/// Writes a small local audit trail for application startup, folder use, recoverable failures, and crashes.
77
/// </summary>
88
public sealed class ApplicationActivityLog
99
{
@@ -33,6 +33,18 @@ public bool TryWriteFolderRestoredFromPreviousSession(string folder)
3333
"Repository folder opened from previous session",
3434
folder);
3535

36+
/// <summary>
37+
/// Records a handled operation failure with its complete exception chain so support can diagnose it.
38+
/// </summary>
39+
public bool TryWriteOperationFailure(string operation, Exception exception)
40+
{
41+
ArgumentException.ThrowIfNullOrWhiteSpace(operation);
42+
ArgumentNullException.ThrowIfNull(exception);
43+
return TryWrite(
44+
$"Operation failed ({operation}):{Environment.NewLine}{Truncate(exception.ToString())}",
45+
flushToDisk: true);
46+
}
47+
3648
public bool TryWriteUnhandledException(string source, Exception exception)
3749
{
3850
ArgumentException.ThrowIfNullOrWhiteSpace(source);

‎src/NoteManager.Core/ViewModels/MainViewModel.cs‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
1313
private readonly RangeObservableCollection<NoteItem> _allNotes;
1414
private readonly RangeObservableCollection<NoteItem> _visibleNotes;
1515
private readonly InfostackerPublishingService _infostackerPublishingService;
16+
private readonly ApplicationActivityLog _activityLog;
1617
private CancellationTokenSource? _indexCancellation;
1718
private CancellationTokenSource? _mediaRefreshCancellation;
1819
private CancellationTokenSource? _publishCancellation;
@@ -46,17 +47,24 @@ public sealed class MainViewModel : ObservableObject, IDisposable
4647
private const string UntaggedFilterKey = "__untagged__";
4748

4849
public MainViewModel(
49-
InfostackerPublishingService? infostackerPublishingService = null)
50-
: this(infostackerPublishingService, useSampleDataForTesting: false)
50+
InfostackerPublishingService? infostackerPublishingService = null,
51+
ApplicationActivityLog? activityLog = null)
52+
: this(
53+
infostackerPublishingService,
54+
activityLog,
55+
useSampleDataForTesting: false)
5156
{
5257
}
5358

5459
private MainViewModel(
5560
InfostackerPublishingService? infostackerPublishingService,
61+
ApplicationActivityLog? activityLog,
5662
bool useSampleDataForTesting)
5763
{
5864
_infostackerPublishingService =
5965
infostackerPublishingService ?? new InfostackerPublishingService();
66+
// Share failures are recoverable, but still need the application log's full exception details.
67+
_activityLog = activityLog ?? new ApplicationActivityLog();
6068
_allNotes = new RangeObservableCollection<NoteItem>();
6169
_visibleNotes = new RangeObservableCollection<NoteItem>();
6270
NavigationItems = [];
@@ -97,8 +105,9 @@ private MainViewModel(
97105
/// need representative note data without opening a user folder.
98106
/// </summary>
99107
public static MainViewModel CreateWithSampleDataForTesting(
100-
InfostackerPublishingService? infostackerPublishingService = null)
101-
=> new(infostackerPublishingService, useSampleDataForTesting: true);
108+
InfostackerPublishingService? infostackerPublishingService = null,
109+
ApplicationActivityLog? activityLog = null)
110+
=> new(infostackerPublishingService, activityLog, useSampleDataForTesting: true);
102111

103112
public ObservableCollection<NavigationItem> NavigationItems { get; }
104113
public ObservableCollection<NoteItem> NotesView => _visibleNotes;
@@ -850,6 +859,8 @@ private static void DeleteCopiedImports(IEnumerable<ImportedPdf> importedPdfs)
850859
}
851860
catch (InfostackerPublishingException exception)
852861
{
862+
// Preserve the wrapped transport or parsing cause while the dialog shows a friendly message.
863+
_activityLog.TryWriteOperationFailure("Publishing a public link", exception);
853864
ShareStatusText = exception.Message;
854865
SetStatus(exception.Message);
855866
return null;
@@ -868,8 +879,12 @@ public void ConfirmPublicLinkCopied(string publicUrl)
868879
SetStatus($"Published note to Infostacker: {publicUrl}");
869880
}
870881

871-
public void ReportClipboardFailure(string message)
882+
public void ReportClipboardFailure(string message, Exception? exception = null)
872883
{
884+
// Clipboard failures occur after publishing, so record them separately from upload failures.
885+
_activityLog.TryWriteOperationFailure(
886+
"Copying a public link to the clipboard",
887+
exception ?? new InvalidOperationException(message));
873888
ShareStatusText =
874889
$"The note was published, but the public link could not be copied: {message}";
875890
SetStatus("Note published, but copying the public link failed");

‎src/NoteManager.Desktop/Dialogs/ShareDialog.axaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- Keep the modal sharing workflow visible instead of allowing it to be hidden in the taskbar. -->
12
<Window xmlns="https://github.com/avaloniaui"
23
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
34
xmlns:desktop="using:NoteManager.Desktop"
@@ -6,6 +7,7 @@
67
Width="640"
78
Height="250"
89
CanResize="False"
10+
CanMinimize="False"
911
Background="{DynamicResource AppBackgroundBrush}"
1012
WindowStartupLocation="CenterOwner"
1113
Title="Share note">

‎src/NoteManager.Desktop/Dialogs/ShareDialog.axaml.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ private async void Publish_OnClick(object? sender, RoutedEventArgs e)
4646
}
4747
catch (Exception exception)
4848
{
49-
ViewModel.ReportClipboardFailure(exception.Message);
49+
// Pass the exception so the application log retains its complete clipboard failure details.
50+
ViewModel.ReportClipboardFailure(exception.Message, exception);
5051
}
5152
}
5253

‎tests/NoteManager.App.Tests/StartupDataTests.cs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,48 @@ public void ApplicationActivityLog_RecordsUnhandledExceptionTypeMessageAndStack(
8888
}
8989
}
9090

91+
[Fact]
92+
public void ApplicationActivityLog_RecordsHandledPublishFailureWithInnerException()
93+
{
94+
var logDirectory = Path.Combine(
95+
Path.GetTempPath(),
96+
$"NoteManager.ApplicationActivityLogPublish.{Guid.NewGuid():N}");
97+
98+
try
99+
{
100+
Exception caught;
101+
try
102+
{
103+
throw new InvalidOperationException(
104+
"Publishing failed",
105+
new HttpRequestException("The sharing service could not be reached."));
106+
}
107+
catch (InvalidOperationException exception)
108+
{
109+
caught = exception;
110+
}
111+
112+
// Recoverable sharing errors must retain their underlying network cause in the daily log.
113+
Assert.True(new ApplicationActivityLog(logDirectory)
114+
.TryWriteOperationFailure("Publishing a public link", caught));
115+
116+
var text = File.ReadAllText(Path.Combine(
117+
logDirectory,
118+
$"{ApplicationActivityLog.LogFilePrefix}{DateTime.Today:yyyy-MM-dd}.log"));
119+
Assert.Contains("Operation failed (Publishing a public link):", text);
120+
Assert.Contains("System.InvalidOperationException: Publishing failed", text);
121+
Assert.Contains("System.Net.Http.HttpRequestException", text);
122+
Assert.Contains("The sharing service could not be reached.", text);
123+
}
124+
finally
125+
{
126+
if (Directory.Exists(logDirectory))
127+
{
128+
Directory.Delete(logDirectory, recursive: true);
129+
}
130+
}
131+
}
132+
91133
[Fact]
92134
public void ApplicationActivityLog_TruncatesOversizedCrashText()
93135
{

0 commit comments

Comments
 (0)