Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions src/BloomExe/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,60 @@ private static bool IsSameActualLanguage(string code1, string code2)
// Only the token owner may release it and run Bloom's global temp cleanup on exit.
private static bool _ownsSingleInstanceToken;

/// <summary>
/// Decides whether a Sentry event is the benign "unobserved Task socket/IO abort" noise
/// that we want to drop rather than report. Used by the BeforeSend filter installed in
/// SetUpErrorHandling.
///
/// Background: the Sentry .NET SDK auto-subscribes to TaskScheduler.UnobservedTaskException.
/// Fleck (our WebSocket library, see BloomWebSocketServer) does fire-and-forget socket.Send()
/// calls whose faulted Tasks are never observed; when a socket aborts (mostly at app shutdown)
/// the finalizer thread surfaces a benign SocketException/IOException and Sentry reports it.
/// An earlier IsAvailable-check mitigation (BL-11124) did not stop it, so tens of thousands of
/// these events accumulated with zero user impact (Sentry BLOOM-DESKTOP-EQ4 / -E4J / -E9K).
///
/// We drop ONLY this exact pattern - an event carrying the UnobservedTaskException mechanism
/// whose exception chain contains a benign socket/IO abort type - so genuine unobserved-Task
/// bugs are still reported. The decision is carried by exception TYPE plus the mechanism, never
/// by message text: the message ("The I/O operation has been aborted...") is localized by the
/// user's OS language, which is why it showed up as several separate Sentry issues.
/// </summary>
internal static bool IsBenignUnobservedTaskSocketNoise(SentryEvent sentryEvent)
{
var exceptions = sentryEvent.SentryExceptions?.ToList();
if (exceptions == null || exceptions.Count == 0)
return false;

// Must have come through the TaskScheduler.UnobservedTaskException integration.
var isUnobservedTask = exceptions.Any(e =>
Comment thread
hatton marked this conversation as resolved.
e.Mechanism?.Type == "UnobservedTaskException"
);
if (!isUnobservedTask)
return false;

// ...and the underlying fault must be a benign socket/IO abort (match on type, not text).
return exceptions.Any(e => IsBenignSocketAbortExceptionType(e.Type));
}

/// <summary>
/// True for the exception types that represent a benign socket/IO abort or cancellation
Comment thread
hatton marked this conversation as resolved.
/// (as opposed to something we would actually want to know about). Matched by full type
/// name so it is independent of the OS-localized exception message.
/// </summary>
private static bool IsBenignSocketAbortExceptionType(string exceptionTypeName)
{
switch (exceptionTypeName)
{
case "System.Net.Sockets.SocketException":
case "System.IO.IOException":
case "System.OperationCanceledException":
case "System.Threading.Tasks.TaskCanceledException":
return true;
default:
return false;
}
}

/// ------------------------------------------------------------------------------------
internal static void SetUpErrorHandling()
{
Expand All @@ -2146,9 +2200,15 @@ internal static void SetUpErrorHandling()
{
try
{
_sentry = SentrySdk.Init(
"https://bba22972ad6b4c2ab03a056f549cc23d@o1009031.ingest.sentry.io/5983534"
);
_sentry = SentrySdk.Init(options =>
{
options.Dsn =
"https://bba22972ad6b4c2ab03a056f549cc23d@o1009031.ingest.sentry.io/5983534";
// Screen out the benign unobserved-Task socket/IO abort noise
// (Sentry BLOOM-DESKTOP-EQ4/E4J/E9K). See IsBenignUnobservedTaskSocketNoise.
options.BeforeSend = sentryEvent =>
IsBenignUnobservedTaskSocketNoise(sentryEvent) ? null : sentryEvent;
});
SentrySdk.ConfigureScope(scope =>
{
scope.SetExtra("channel", ApplicationUpdateSupport.ChannelName);
Expand Down
105 changes: 105 additions & 0 deletions src/BloomTests/ProgramTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Bloom;
using NUnit.Framework;
using Sentry;
using Sentry.Protocol;

namespace BloomTests
{
Expand Down Expand Up @@ -132,5 +134,108 @@ out var errorMessage
Assert.That(Program.StartupLabel, Is.Null);
Comment thread
hatton marked this conversation as resolved.
Assert.That(remainingArgs, Is.Empty);
}

// --- IsBenignUnobservedTaskSocketNoise: the Sentry BeforeSend filter for
// BLOOM-DESKTOP-EQ4 / -E4J / -E9K ---

private static SentryException MakeException(
string type,
string value = null,
string mechanismType = null
)
{
var exception = new SentryException { Type = type, Value = value };
if (mechanismType != null)
exception.Mechanism = new Mechanism { Type = mechanismType, Handled = false };
return exception;
}

private static SentryEvent MakeEvent(params SentryException[] exceptions)
{
return new SentryEvent { SentryExceptions = exceptions };
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_DropsUnobservedSocketAbort()
{
// The shape Sentry actually serialized for BLOOM-DESKTOP-EQ4: an inner SocketException,
// wrapped in an AggregateException carrying the UnobservedTaskException mechanism.
var sentryEvent = MakeEvent(
MakeException(
"System.Net.Sockets.SocketException",
"The I/O operation has been aborted because of either a thread exit or an application request."
),
MakeException(
"System.AggregateException",
"A Task's exception(s) were not observed...",
"UnobservedTaskException"
)
);

// Sanity check the setup before asserting the method's behavior.
Assert.That(
sentryEvent.SentryExceptions,
Is.Not.Empty,
"test setup should produce an exception chain"
);

Assert.That(Program.IsBenignUnobservedTaskSocketNoise(sentryEvent), Is.True);
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_DropsUnobservedIoAbort()
{
var sentryEvent = MakeEvent(
MakeException("System.IO.IOException", "aborted"),
MakeException("System.AggregateException", null, "UnobservedTaskException")
);

Assert.That(Program.IsBenignUnobservedTaskSocketNoise(sentryEvent), Is.True);
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_DropsRegardlessOfLocalizedMessage()
{
// The message is localized by the user's OS (this is the Spanish variant,
// BLOOM-DESKTOP-E4J). The filter must decide on TYPE + mechanism, not text.
var sentryEvent = MakeEvent(
MakeException(
"System.Net.Sockets.SocketException",
"Se ha anulado la operacion de E/S debido a la salida del subproceso o a una solicitud de la aplicacion."
),
MakeException("System.AggregateException", null, "UnobservedTaskException")
);

Assert.That(Program.IsBenignUnobservedTaskSocketNoise(sentryEvent), Is.True);
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_KeepsUnobservedTaskThatIsNotSocketNoise()
{
// A genuine unobserved-Task bug (not a socket/IO abort) must still be reported.
var sentryEvent = MakeEvent(
MakeException("System.NullReferenceException", "Object reference not set..."),
MakeException("System.AggregateException", null, "UnobservedTaskException")
);

Assert.That(Program.IsBenignUnobservedTaskSocketNoise(sentryEvent), Is.False);
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_KeepsSocketExceptionWithoutUnobservedMechanism()
{
// A SocketException reported through some other (e.g. handled) path is not this noise.
var sentryEvent = MakeEvent(
MakeException("System.Net.Sockets.SocketException", "connection reset")
);

Assert.That(Program.IsBenignUnobservedTaskSocketNoise(sentryEvent), Is.False);
}

[Test]
public void IsBenignUnobservedTaskSocketNoise_KeepsEventWithNoExceptions()
{
Assert.That(Program.IsBenignUnobservedTaskSocketNoise(new SentryEvent()), Is.False);
}
}
}