Skip to content

Commit 663c95a

Browse files
fix: EventProcessors not running for native crashes on iOS (#4318)
* fix: EventProcessors not running for native crashes on iOS Resolves #3621: - #3621
1 parent cd937db commit 663c95a

File tree

6 files changed

+195
-77
lines changed

6 files changed

+195
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
### Fixes
1414

15+
- Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318))
1516
- Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314))
1617

1718
### Dependencies
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Sentry.Extensibility;
2+
3+
namespace Sentry.Internal;
4+
5+
internal static class SentryEventHelper
6+
{
7+
public static SentryEvent? ProcessEvent(SentryEvent? evt, IEnumerable<ISentryEventProcessor> processors, SentryHint? hint, SentryOptions options)
8+
{
9+
if (evt == null)
10+
{
11+
return evt;
12+
}
13+
14+
var processedEvent = evt;
15+
var effectiveHint = hint ?? new SentryHint(options);
16+
17+
foreach (var processor in processors)
18+
{
19+
processedEvent = processor.DoProcessEvent(processedEvent, effectiveHint);
20+
if (processedEvent == null)
21+
{
22+
options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error);
23+
options.LogInfo("Event dropped by processor {0}", processor.GetType().Name);
24+
break;
25+
}
26+
}
27+
return processedEvent;
28+
}
29+
30+
#if NET6_0_OR_GREATER
31+
[UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = AotHelper.AvoidAtRuntime)]
32+
#endif
33+
public static SentryEvent? DoBeforeSend(SentryEvent? @event, SentryHint hint, SentryOptions options)
34+
{
35+
if (@event is null || options.BeforeSendInternal is null)
36+
{
37+
return @event;
38+
}
39+
40+
options.LogDebug("Calling the BeforeSend callback");
41+
try
42+
{
43+
@event = options.BeforeSendInternal?.Invoke(@event, hint);
44+
if (@event == null) // Rejected event
45+
{
46+
options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Error);
47+
options.LogInfo("Event dropped by BeforeSend callback.");
48+
}
49+
}
50+
catch (Exception e)
51+
{
52+
if (!AotHelper.IsTrimmed)
53+
{
54+
// Attempt to demystify exceptions before adding them as breadcrumbs.
55+
e.Demystify();
56+
}
57+
58+
options.LogError(e, "The BeforeSend callback threw an exception. It will be added as breadcrumb and continue.");
59+
var data = new Dictionary<string, string>
60+
{
61+
{"message", e.Message}
62+
};
63+
if (e.StackTrace is not null)
64+
{
65+
data.Add("stackTrace", e.StackTrace);
66+
}
67+
@event?.AddBreadcrumb(
68+
"BeforeSend callback failed.",
69+
category: "SentryClient",
70+
data: data,
71+
level: BreadcrumbLevel.Error);
72+
}
73+
74+
return @event;
75+
}
76+
}

src/Sentry/Platforms/Cocoa/SentrySdk.cs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Sentry.Cocoa;
22
using Sentry.Cocoa.Extensions;
33
using Sentry.Extensibility;
4+
using Sentry.Internal;
45

56
// ReSharper disable once CheckNamespace
67
namespace Sentry;
@@ -188,8 +189,21 @@ private static CocoaSdk.SentryHttpStatusCodeRange[] GetFailedRequestStatusCodes(
188189
return nativeRanges;
189190
}
190191

192+
[DebuggerStepThrough]
191193
internal static CocoaSdk.SentryEvent? ProcessOnBeforeSend(SentryOptions options, CocoaSdk.SentryEvent evt)
194+
=> ProcessOnBeforeSend(options, evt, CurrentHub);
195+
196+
/// <summary>
197+
/// This overload allows us to inject an IHub for testing. During normal execution, the CurrentHub is used.
198+
/// However, since this class is static, there's no easy alternative way to inject this when executing tests.
199+
/// </summary>
200+
internal static CocoaSdk.SentryEvent? ProcessOnBeforeSend(SentryOptions options, CocoaSdk.SentryEvent evt, IHub hub)
192201
{
202+
if (hub is DisabledHub)
203+
{
204+
return evt;
205+
}
206+
193207
// When we have an unhandled managed exception, we send that to Sentry twice - once managed and once native.
194208
// The managed exception is what a .NET developer would expect, and it is sent by the Sentry.NET SDK
195209
// But we also get a native SIGABRT since it crashed the application, which is sent by the Sentry Cocoa SDK.
@@ -224,32 +238,52 @@ private static CocoaSdk.SentryHttpStatusCodeRange[] GetFailedRequestStatusCodes(
224238
}
225239
}
226240

227-
// we run our SIGABRT checks first before handing over to user events
228-
// because we delegate to user code, we need to protect anything that could happen in this event
229-
if (options.BeforeSendInternal == null)
230-
return evt;
231-
241+
// We run our SIGABRT checks first before running managed processors.
242+
// Because we delegate to user code, we need to catch/log exceptions.
232243
try
233244
{
234-
var sentryEvent = evt.ToSentryEvent();
235-
if (sentryEvent == null)
245+
// Normally the event processors would be invoked by the SentryClient, but the Cocoa SDK has its own client,
246+
// so we need to manually invoke any managed event processors here to apply them to Native events.
247+
var manualProcessors = GetEventProcessors(hub)
248+
.Where(p => p is not MainSentryEventProcessor)
249+
.ToArray();
250+
if (manualProcessors.Length == 0 && options.BeforeSendInternal is null)
251+
{
236252
return evt;
253+
}
237254

238-
var result = options.BeforeSendInternal(sentryEvent, null!);
239-
if (result == null)
240-
return null!;
255+
var sentryEvent = evt.ToSentryEvent();
256+
if (SentryEventHelper.ProcessEvent(sentryEvent, manualProcessors, null, options) is not { } processedEvent)
257+
{
258+
return null;
259+
}
260+
261+
processedEvent = SentryEventHelper.DoBeforeSend(processedEvent, new SentryHint(), options);
262+
if (processedEvent == null)
263+
{
264+
return null;
265+
}
241266

242267
// we only support a subset of mutated data to be passed back to the native SDK at this time
243-
result.CopyToCocoaSentryEvent(evt);
268+
processedEvent.CopyToCocoaSentryEvent(evt);
244269

245-
// Note: Nullable result is allowed but delegate is generated incorrectly
246-
// See https://github.com/xamarin/xamarin-macios/issues/15299#issuecomment-1201863294
247-
return evt!;
270+
return evt;
248271
}
249272
catch (Exception ex)
250273
{
251-
options.LogError(ex, "Before Send Error");
274+
options.LogError(ex, "Error running managed event processors for native event");
252275
return evt;
253276
}
277+
278+
static IEnumerable<ISentryEventProcessor> GetEventProcessors(IHub hub)
279+
{
280+
if (hub is Hub fullHub)
281+
{
282+
return fullHub.ScopeManager.GetCurrent().Key.GetAllEventProcessors();
283+
}
284+
IEnumerable<ISentryEventProcessor>? eventProcessors = null;
285+
hub.ConfigureScope(scope => eventProcessors = scope.GetAllEventProcessors());
286+
return eventProcessors ?? [];
287+
}
254288
}
255289
}

src/Sentry/SentryClient.cs

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -348,26 +348,15 @@ private SentryId DoSendEvent(SentryEvent @event, SentryHint? hint, Scope? scope)
348348
}
349349
}
350350

351-
var processedEvent = @event;
352-
353-
foreach (var processor in scope.GetAllEventProcessors())
351+
if (SentryEventHelper.ProcessEvent(@event, scope.GetAllEventProcessors(), hint, _options) is not { } processedEvent)
354352
{
355-
processedEvent = processor.DoProcessEvent(processedEvent, hint);
356-
357-
if (processedEvent == null)
358-
{
359-
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.EventProcessor, DataCategory.Error);
360-
_options.LogInfo("Event dropped by processor {0}", processor.GetType().Name);
361-
return SentryId.Empty;
362-
}
353+
return SentryId.Empty; // Dropped by an event processor
363354
}
364355

365-
processedEvent = BeforeSend(processedEvent, hint);
366-
if (processedEvent == null) // Rejected event
356+
processedEvent = SentryEventHelper.DoBeforeSend(processedEvent, hint, _options);
357+
if (processedEvent == null)
367358
{
368-
_options.ClientReportRecorder.RecordDiscardedEvent(DiscardReason.BeforeSend, DataCategory.Error);
369-
_options.LogInfo("Event dropped by BeforeSend callback.");
370-
return SentryId.Empty;
359+
return SentryId.Empty; // Dropped by BeforeSend callback
371360
}
372361

373362
var hasTerminalException = processedEvent.HasTerminalException();
@@ -454,48 +443,6 @@ public bool CaptureEnvelope(Envelope envelope)
454443
return false;
455444
}
456445

457-
#if NET6_0_OR_GREATER
458-
[UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = AotHelper.AvoidAtRuntime)]
459-
#endif
460-
private SentryEvent? BeforeSend(SentryEvent? @event, SentryHint hint)
461-
{
462-
if (_options.BeforeSendInternal == null)
463-
{
464-
return @event;
465-
}
466-
467-
_options.LogDebug("Calling the BeforeSend callback");
468-
try
469-
{
470-
@event = _options.BeforeSendInternal?.Invoke(@event!, hint);
471-
}
472-
catch (Exception e)
473-
{
474-
if (!AotHelper.IsTrimmed)
475-
{
476-
// Attempt to demystify exceptions before adding them as breadcrumbs.
477-
e.Demystify();
478-
}
479-
480-
_options.LogError(e, "The BeforeSend callback threw an exception. It will be added as breadcrumb and continue.");
481-
var data = new Dictionary<string, string>
482-
{
483-
{"message", e.Message}
484-
};
485-
if (e.StackTrace is not null)
486-
{
487-
data.Add("stackTrace", e.StackTrace);
488-
}
489-
@event?.AddBreadcrumb(
490-
"BeforeSend callback failed.",
491-
category: "SentryClient",
492-
data: data,
493-
level: BreadcrumbLevel.Error);
494-
}
495-
496-
return @event;
497-
}
498-
499446
/// <summary>
500447
/// Disposes this client
501448
/// </summary>

test/Sentry.Tests/SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb.verified.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
message: Exception message!,
77
stackTrace:
88
at Task Sentry.Tests.SentryClientTests.CaptureEvent_BeforeEventThrows_ErrorToEventBreadcrumb()
9-
at SentryEvent Sentry.SentryClient.BeforeSend(...)
9+
at SentryEvent Sentry.Internal.SentryEventHelper.DoBeforeSend(...)
1010
},
1111
Category: SentryClient,
1212
Level: error

test/Sentry.Tests/SentrySdkTests.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,7 @@ public void InitHub_DebugEnabled_DebugLogsLogged()
996996
[InlineData(false)]
997997
public void ProcessOnBeforeSend_NativeErrorSuppression(bool suppressNativeErrors)
998998
{
999+
// Arrange
9991000
var options = new SentryOptions
10001001
{
10011002
Dsn = ValidDsn,
@@ -1014,11 +1015,20 @@ public void ProcessOnBeforeSend_NativeErrorSuppression(bool suppressNativeErrors
10141015
called = true;
10151016
return e;
10161017
});
1018+
1019+
var scope = new Scope(options);
1020+
var hub = Substitute.For<IHub>();
1021+
hub.When(h => hub.ConfigureScope(Arg.Any<Action<Scope>>()))
1022+
.Do(callback => callback.Arg<Action<Scope>>().Invoke(scope));
1023+
10171024
var evt = new Sentry.CocoaSdk.SentryEvent();
10181025
var ex = new Sentry.CocoaSdk.SentryException("Not checked", "EXC_BAD_ACCESS");
10191026
evt.Exceptions = [ex];
1020-
var result = SentrySdk.ProcessOnBeforeSend(options, evt);
10211027

1028+
// Act
1029+
var result = SentrySdk.ProcessOnBeforeSend(options, evt, hub);
1030+
1031+
// Assert
10221032
if (suppressNativeErrors)
10231033
{
10241034
called.Should().BeFalse();
@@ -1034,6 +1044,7 @@ public void ProcessOnBeforeSend_NativeErrorSuppression(bool suppressNativeErrors
10341044
[Fact]
10351045
public void ProcessOnBeforeSend_OptionsBeforeOnSendRuns()
10361046
{
1047+
// Arrange
10371048
var options = new SentryOptions
10381049
{
10391050
Dsn = ValidDsn,
@@ -1052,20 +1063,69 @@ public void ProcessOnBeforeSend_OptionsBeforeOnSendRuns()
10521063
native.ReleaseName = "release name";
10531064
native.Environment = "environment";
10541065
native.Transaction = "transaction name";
1055-
10561066
options.SetBeforeSend(e =>
10571067
{
10581068
e.TransactionName = "dotnet";
10591069
return e;
10601070
});
1061-
var result = SentrySdk.ProcessOnBeforeSend(options, native);
1071+
1072+
var scope = new Scope(options);
1073+
var hub = Substitute.For<IHub>();
1074+
hub.When(h => hub.ConfigureScope(Arg.Any<Action<Scope>>()))
1075+
.Do(callback => callback.Arg<Action<Scope>>().Invoke(scope));
1076+
1077+
// Act
1078+
var result = SentrySdk.ProcessOnBeforeSend(options, native, hub);
1079+
1080+
// Assert
10621081
result.Should().NotBeNull();
10631082
result.Transaction.Should().Be("dotnet");
10641083
}
1084+
1085+
[Fact]
1086+
public void ProcessOnBeforeSend_EventProcessorsInvoked()
1087+
{
1088+
// Arrange
1089+
var options = new SentryOptions
1090+
{
1091+
Dsn = ValidDsn,
1092+
DiagnosticLogger = _logger,
1093+
IsGlobalModeEnabled = true,
1094+
Debug = true,
1095+
AutoSessionTracking = false,
1096+
BackgroundWorker = Substitute.For<IBackgroundWorker>(),
1097+
InitNativeSdks = false,
1098+
};
1099+
var eventProcessor = new TestEventProcessor();
1100+
options.AddEventProcessor(eventProcessor);
1101+
1102+
var scope = new Scope(options);
1103+
var hub = Substitute.For<IHub>();
1104+
hub.When(h => hub.ConfigureScope(Arg.Any<Action<Scope>>()))
1105+
.Do(callback => callback.Arg<Action<Scope>>().Invoke(scope));
1106+
1107+
var native = new Sentry.CocoaSdk.SentryEvent();
1108+
1109+
// Act
1110+
SentrySdk.ProcessOnBeforeSend(options, native, hub);
1111+
1112+
// Assert
1113+
eventProcessor.Invoked.Should().BeTrue();
1114+
}
10651115
#endif
10661116

10671117
public void Dispose()
10681118
{
10691119
SentrySdk.Close();
10701120
}
10711121
}
1122+
1123+
file class TestEventProcessor : ISentryEventProcessor
1124+
{
1125+
public bool Invoked { get; private set; }
1126+
public SentryEvent Process(SentryEvent @event)
1127+
{
1128+
Invoked = true;
1129+
return @event;
1130+
}
1131+
}

0 commit comments

Comments
 (0)