Skip to content

Commit ff04d22

Browse files
committed
Update
1 parent df9c16e commit ff04d22

8 files changed

+207
-14
lines changed

dart/lib/src/exception_type_identifier.dart

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:meta/meta.dart';
2+
13
/// An abstract class for identifying the type of Dart errors and exceptions.
24
///
35
/// It's used in scenarios where error types need to be determined in obfuscated builds
@@ -6,7 +8,7 @@
68
/// Implement this class to create custom error type identifiers for errors or exceptions.
79
/// that we do not support out of the box.
810
///
9-
/// Add the implementation using [SentryOptions.addExceptionTypeIdentifier].
11+
/// Add the implementation using [SentryOptions.prependExceptionTypeIdentifier].
1012
///
1113
/// Example:
1214
/// ```dart
@@ -23,14 +25,15 @@ abstract class ExceptionTypeIdentifier {
2325
}
2426

2527
extension CacheableExceptionIdentifier on ExceptionTypeIdentifier {
26-
ExceptionTypeIdentifier withCache() => _CachingExceptionTypeIdentifier(this);
28+
ExceptionTypeIdentifier withCache() => CachingExceptionTypeIdentifier(this);
2729
}
2830

29-
class _CachingExceptionTypeIdentifier implements ExceptionTypeIdentifier {
31+
@visibleForTesting
32+
class CachingExceptionTypeIdentifier implements ExceptionTypeIdentifier {
3033
final ExceptionTypeIdentifier _identifier;
3134
final Map<Type, String?> _knownExceptionTypes = {};
3235

33-
_CachingExceptionTypeIdentifier(this._identifier);
36+
CachingExceptionTypeIdentifier(this._identifier);
3437

3538
@override
3639
String? identifyType(dynamic throwable) {

dart/lib/src/sentry.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Sentry {
8787
options.addEventProcessor(ExceptionEventProcessor(options));
8888
options.addEventProcessor(DeduplicationEventProcessor(options));
8989

90-
options.addExceptionTypeIdentifier(DartExceptionTypeIdentifier());
90+
options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());
9191
}
9292

9393
/// This method reads available environment variables and uses them

dart/lib/src/sentry_exception_factory.dart

+8-5
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,14 @@ class SentryExceptionFactory {
6262
final value = throwableString.replaceAll(stackTraceString, '').trim();
6363

6464
String errorTypeName = throwable.runtimeType.toString();
65-
for (final errorTypeIdentifier in _options.exceptionTypeIdentifiers) {
66-
final identifiedErrorType = errorTypeIdentifier.identifyType(throwable);
67-
if (identifiedErrorType != null) {
68-
errorTypeName = identifiedErrorType;
69-
break;
65+
66+
if (_options.enableExceptionTypeIdentification) {
67+
for (final errorTypeIdentifier in _options.exceptionTypeIdentifiers) {
68+
final identifiedErrorType = errorTypeIdentifier.identifyType(throwable);
69+
if (identifiedErrorType != null) {
70+
errorTypeName = identifiedErrorType;
71+
break;
72+
}
7073
}
7174
}
7275

dart/lib/src/sentry_options.dart

+14-3
Original file line numberDiff line numberDiff line change
@@ -437,20 +437,31 @@ class SentryOptions {
437437
/// Settings this to `false` will set the `level` to [SentryLevel.error].
438438
bool markAutomaticallyCollectedErrorsAsFatal = true;
439439

440+
/// Enables identification of exception types in obfuscated builds.
441+
/// When true, the SDK will attempt to identify common exception types
442+
/// to improve readability of obfuscated issue titles.
443+
///
444+
/// If you already have issues with obfuscated issue titles this will change grouping.
445+
///
446+
/// Default: `true`
447+
bool enableExceptionTypeIdentification = true;
448+
440449
final List<ExceptionTypeIdentifier> _exceptionTypeIdentifiers = [];
441450

442451
List<ExceptionTypeIdentifier> get exceptionTypeIdentifiers =>
443-
_exceptionTypeIdentifiers;
452+
List.unmodifiable(_exceptionTypeIdentifiers);
444453

445454
void addExceptionTypeIdentifierByIndex(
446455
int index, ExceptionTypeIdentifier exceptionTypeIdentifier) {
447456
_exceptionTypeIdentifiers.insert(
448457
index, exceptionTypeIdentifier.withCache());
449458
}
450459

451-
void addExceptionTypeIdentifier(
460+
/// Adds an exception type identifier to the beginning of the list.
461+
/// This ensures it is processed first and takes precedence over existing identifiers.
462+
void prependExceptionTypeIdentifier(
452463
ExceptionTypeIdentifier exceptionTypeIdentifier) {
453-
_exceptionTypeIdentifiers.add(exceptionTypeIdentifier.withCache());
464+
addExceptionTypeIdentifierByIndex(0, exceptionTypeIdentifier);
454465
}
455466

456467
/// The Spotlight configuration.

dart/test/client_reports/client_report_test.dart

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import 'package:sentry/src/transport/data_category.dart';
66
import 'package:test/test.dart';
77
import 'package:sentry/src/utils.dart';
88

9+
class CustomException implements Exception {
10+
message() {
11+
return 'This is a custom exception';
12+
}
13+
}
14+
915
void main() {
1016
group('json', () {
1117
late Fixture fixture;
@@ -15,6 +21,7 @@ void main() {
1521
});
1622

1723
test('toJson', () {
24+
print(CustomException().runtimeType.toString());
1825
final sut = fixture.getSut();
1926
final json = sut.toJson();
2027

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import 'package:mockito/mockito.dart';
2+
import 'package:sentry/sentry.dart';
3+
import 'package:sentry/src/dart_exception_type_identifier.dart';
4+
import 'package:sentry/src/sentry_exception_factory.dart';
5+
import 'package:test/test.dart';
6+
7+
import 'mocks.dart';
8+
import 'mocks.mocks.dart';
9+
import 'mocks/mock_transport.dart';
10+
import 'sentry_client_test.dart';
11+
12+
void main() {
13+
late Fixture fixture;
14+
15+
setUp(() {
16+
fixture = Fixture();
17+
});
18+
19+
group('ExceptionTypeIdentifiers', () {
20+
test('first in the list will be processed first', () {
21+
fixture.options
22+
.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());
23+
fixture.options
24+
.prependExceptionTypeIdentifier(ObfuscatedExceptionIdentifier());
25+
26+
final factory = SentryExceptionFactory(fixture.options);
27+
final sentryException = factory.getSentryException(ObfuscatedException());
28+
29+
expect(sentryException.type, equals('ObfuscatedException'));
30+
});
31+
});
32+
33+
group('CachingExceptionTypeIdentifier', () {
34+
late MockExceptionTypeIdentifier mockIdentifier;
35+
late CachingExceptionTypeIdentifier cachingIdentifier;
36+
37+
setUp(() {
38+
mockIdentifier = MockExceptionTypeIdentifier();
39+
cachingIdentifier = CachingExceptionTypeIdentifier(mockIdentifier);
40+
});
41+
42+
test('should return cached result for known types', () {
43+
final exception = Exception('Test');
44+
when(mockIdentifier.identifyType(exception)).thenReturn('TestException');
45+
46+
expect(
47+
cachingIdentifier.identifyType(exception), equals('TestException'));
48+
expect(
49+
cachingIdentifier.identifyType(exception), equals('TestException'));
50+
expect(
51+
cachingIdentifier.identifyType(exception), equals('TestException'));
52+
53+
verify(mockIdentifier.identifyType(exception)).called(1);
54+
});
55+
56+
test('should not cache unknown types', () {
57+
final exception = ObfuscatedException();
58+
59+
when(mockIdentifier.identifyType(exception)).thenReturn(null);
60+
61+
expect(cachingIdentifier.identifyType(exception), isNull);
62+
expect(cachingIdentifier.identifyType(exception), isNull);
63+
expect(cachingIdentifier.identifyType(exception), isNull);
64+
65+
verify(mockIdentifier.identifyType(exception)).called(3);
66+
});
67+
68+
test('should return null for unknown exception type', () {
69+
final exception = Exception('Unknown');
70+
when(mockIdentifier.identifyType(exception)).thenReturn(null);
71+
72+
expect(cachingIdentifier.identifyType(exception), isNull);
73+
});
74+
75+
test('should handle different exception types separately', () {
76+
final exception1 = Exception('Test1');
77+
final exception2 = FormatException('Test2');
78+
79+
when(mockIdentifier.identifyType(exception1)).thenReturn('Exception');
80+
when(mockIdentifier.identifyType(exception2))
81+
.thenReturn('FormatException');
82+
83+
expect(cachingIdentifier.identifyType(exception1), equals('Exception'));
84+
expect(cachingIdentifier.identifyType(exception2),
85+
equals('FormatException'));
86+
87+
// Call again to test caching
88+
expect(cachingIdentifier.identifyType(exception1), equals('Exception'));
89+
expect(cachingIdentifier.identifyType(exception2),
90+
equals('FormatException'));
91+
92+
verify(mockIdentifier.identifyType(exception1)).called(1);
93+
verify(mockIdentifier.identifyType(exception2)).called(1);
94+
});
95+
});
96+
97+
group('Integration test', () {
98+
setUp(() {
99+
fixture.options.transport = MockTransport();
100+
});
101+
102+
test(
103+
'should capture CustomException as exception type with custom identifier',
104+
() async {
105+
fixture.options
106+
.prependExceptionTypeIdentifier(ObfuscatedExceptionIdentifier());
107+
108+
final client = SentryClient(fixture.options);
109+
110+
await client.captureException(ObfuscatedException());
111+
112+
final transport = fixture.options.transport as MockTransport;
113+
final capturedEnvelope = transport.envelopes.first;
114+
final capturedEvent = await eventFromEnvelope(capturedEnvelope);
115+
116+
expect(
117+
capturedEvent.exceptions!.first.type, equals('ObfuscatedException'));
118+
});
119+
120+
test(
121+
'should capture PlaceHolderException as exception type without custom identifier',
122+
() async {
123+
final client = SentryClient(fixture.options);
124+
125+
await client.captureException(ObfuscatedException());
126+
127+
final transport = fixture.options.transport as MockTransport;
128+
final capturedEnvelope = transport.envelopes.first;
129+
final capturedEvent = await eventFromEnvelope(capturedEnvelope);
130+
131+
expect(
132+
capturedEvent.exceptions!.first.type, equals('PlaceHolderException'));
133+
});
134+
});
135+
}
136+
137+
class Fixture {
138+
SentryOptions options = SentryOptions(dsn: fakeDsn);
139+
}
140+
141+
// We use this PlaceHolder exception to mimic an obfuscated runtimeType
142+
class PlaceHolderException implements Exception {}
143+
144+
class ObfuscatedException implements Exception {
145+
@override
146+
Type get runtimeType => PlaceHolderException;
147+
}
148+
149+
class ObfuscatedExceptionIdentifier implements ExceptionTypeIdentifier {
150+
@override
151+
String? identifyType(dynamic throwable) {
152+
if (throwable is ObfuscatedException) return 'ObfuscatedException';
153+
return null;
154+
}
155+
}

dart/test/mocks.dart

+1
Original file line numberDiff line numberDiff line change
@@ -207,5 +207,6 @@ class MockRateLimiter implements RateLimiter {
207207
SentryProfilerFactory,
208208
SentryProfiler,
209209
SentryProfileInfo,
210+
ExceptionTypeIdentifier,
210211
])
211212
void main() {}

dart/test/mocks.mocks.dart

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Mocks generated by Mockito 5.4.2 from annotations
1+
// Mocks generated by Mockito 5.4.4 from annotations
22
// in sentry/test/mocks.dart.
33
// Do not manually edit this file.
44

@@ -13,6 +13,8 @@ import 'package:sentry/src/profiling.dart' as _i3;
1313
// ignore_for_file: avoid_redundant_argument_values
1414
// ignore_for_file: avoid_setters_without_getters
1515
// ignore_for_file: comment_references
16+
// ignore_for_file: deprecated_member_use
17+
// ignore_for_file: deprecated_member_use_from_same_package
1618
// ignore_for_file: implementation_imports
1719
// ignore_for_file: invalid_use_of_visible_for_testing_member
1820
// ignore_for_file: prefer_const_constructors
@@ -66,6 +68,7 @@ class MockSentryProfiler extends _i1.Mock implements _i3.SentryProfiler {
6668
),
6769
returnValue: _i4.Future<_i3.SentryProfileInfo?>.value(),
6870
) as _i4.Future<_i3.SentryProfileInfo?>);
71+
6972
@override
7073
void dispose() => super.noSuchMethod(
7174
Invocation.method(
@@ -99,3 +102,13 @@ class MockSentryProfileInfo extends _i1.Mock implements _i3.SentryProfileInfo {
99102
),
100103
) as _i2.SentryEnvelopeItem);
101104
}
105+
106+
/// A class which mocks [ExceptionTypeIdentifier].
107+
///
108+
/// See the documentation for Mockito's code generation for more information.
109+
class MockExceptionTypeIdentifier extends _i1.Mock
110+
implements _i2.ExceptionTypeIdentifier {
111+
MockExceptionTypeIdentifier() {
112+
_i1.throwOnMissingStub(this);
113+
}
114+
}

0 commit comments

Comments
 (0)