diff --git a/CHANGELOG.md b/CHANGELOG.md index 66a553530f..6797e66cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Support beforeSendTransaction ([#1238](https://github.com/getsentry/sentry-dart/pull/1238)) - Add In Foreground to App context ([#1260](https://github.com/getsentry/sentry-dart/pull/1260)) ### Fixes diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 75698d8322..63be0ae39a 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -87,7 +87,7 @@ class SentryClient { return _sentryId; } - preparedEvent = await _processEvent( + preparedEvent = await _runEventProcessors( preparedEvent, eventProcessors: _options.eventProcessors, hint: hint, @@ -98,27 +98,14 @@ class SentryClient { return _sentryId; } - final beforeSend = _options.beforeSend; - if (beforeSend != null) { - final beforeSendEvent = preparedEvent; - try { - preparedEvent = await beforeSend(preparedEvent, hint: hint); - } catch (exception, stackTrace) { - _options.logger( - SentryLevel.error, - 'The BeforeSend callback threw an exception', - exception: exception, - stackTrace: stackTrace, - ); - } - if (preparedEvent == null) { - _recordLostEvent(beforeSendEvent, DiscardReason.beforeSend); - _options.logger( - SentryLevel.debug, - 'Event was dropped by BeforeSend callback', - ); - return _sentryId; - } + preparedEvent = await _runBeforeSend( + preparedEvent, + hint: hint, + ); + + // dropped by beforeSend + if (preparedEvent == null) { + return _sentryId; } if (_options.platformChecker.platform.isAndroid && @@ -324,7 +311,7 @@ class SentryClient { return _sentryId; } - preparedTransaction = await _processEvent( + preparedTransaction = await _runEventProcessors( preparedTransaction, eventProcessors: _options.eventProcessors, ) as SentryTransaction?; @@ -334,6 +321,14 @@ class SentryClient { return _sentryId; } + preparedTransaction = + await _runBeforeSend(preparedTransaction) as SentryTransaction?; + + // dropped by beforeSendTransaction + if (preparedTransaction == null) { + return _sentryId; + } + final attachments = scope?.attachments .where((element) => element.addToTransactions) .toList(); @@ -366,7 +361,44 @@ class SentryClient { void close() => _options.httpClient.close(); - Future _processEvent( + Future _runBeforeSend( + SentryEvent event, { + Hint? hint, + }) async { + SentryEvent? eventOrTransaction = event; + + final beforeSend = _options.beforeSend; + final beforeSendTransaction = _options.beforeSendTransaction; + String beforeSendName = 'beforeSend'; + + try { + if (event is SentryTransaction && beforeSendTransaction != null) { + beforeSendName = 'beforeSendTransaction'; + eventOrTransaction = await beforeSendTransaction(event); + } else if (beforeSend != null) { + eventOrTransaction = await beforeSend(event, hint: hint); + } + } catch (exception, stackTrace) { + _options.logger( + SentryLevel.error, + 'The $beforeSendName callback threw an exception', + exception: exception, + stackTrace: stackTrace, + ); + } + + if (eventOrTransaction == null) { + _recordLostEvent(event, DiscardReason.beforeSend); + _options.logger( + SentryLevel.debug, + '${event.runtimeType} was dropped by $beforeSendName callback', + ); + } + + return eventOrTransaction; + } + + Future _runEventProcessors( SentryEvent event, { Hint? hint, required List eventProcessors, diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2018f9ddc1..93a7f80bdc 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -150,6 +150,10 @@ class SentryOptions { /// object or nothing to skip reporting the event BeforeSendCallback? beforeSend; + /// This function is called with an SDK specific transaction object and can return a modified + /// transaction object or nothing to skip reporting the transaction + BeforeSendTransactionCallback? beforeSendTransaction; + /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped BeforeBreadcrumbCallback? beforeBreadcrumb; @@ -425,6 +429,12 @@ typedef BeforeSendCallback = FutureOr Function( Hint? hint, }); +/// This function is called with an SDK specific transaction object and can return a modified transaction +/// object or nothing to skip reporting the transaction +typedef BeforeSendTransactionCallback = FutureOr Function( + SentryTransaction transaction, +); + /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped typedef BeforeBreadcrumbCallback = Breadcrumb? Function( diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index f5f9263824..776726419b 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -894,6 +894,74 @@ void main() { }); }); + group('SentryClient before send transaction', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('before send transaction drops event', () async { + final client = fixture.getSut( + beforeSendTransaction: beforeSendTransactionCallbackDropEvent); + final fakeTransaction = fixture.fakeTransaction(); + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(0), true); + }); + + test('async before send transaction drops event', () async { + final client = fixture.getSut( + beforeSendTransaction: asyncBeforeSendTransactionCallbackDropEvent); + final fakeTransaction = fixture.fakeTransaction(); + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(0), true); + }); + + test( + 'before send transaction returns an transaction and transaction is captured', + () async { + final client = + fixture.getSut(beforeSendTransaction: beforeSendTransactionCallback); + final fakeTransaction = fixture.fakeTransaction(); + await client.captureTransaction(fakeTransaction); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final transaction = await transactionFromEnvelope(capturedEnvelope); + + expect(transaction['tags']!.containsKey('theme'), true); + expect(transaction['extra']!.containsKey('host'), true); + expect(transaction['sdk']!['integrations'].contains('testIntegration'), + true); + expect( + transaction['sdk']!['packages'] + .any((element) => element['name'] == 'test-pkg'), + true, + ); + expect( + transaction['breadcrumbs']! + .any((element) => element['message'] == 'processor crumb'), + true, + ); + }); + + test('thrown error is handled', () async { + final exception = Exception("before send exception"); + final beforeSendTransactionCallback = (SentryTransaction event) { + throw exception; + }; + + final client = fixture.getSut( + beforeSendTransaction: beforeSendTransactionCallback, debug: true); + final fakeTransaction = fixture.fakeTransaction(); + await client.captureTransaction(fakeTransaction); + + expect(fixture.loggedException, exception); + expect(fixture.loggedLevel, SentryLevel.error); + }); + }); + group('SentryClient before send', () { late Fixture fixture; @@ -1439,6 +1507,11 @@ FutureOr beforeSendCallbackDropEvent( }) => null; +FutureOr beforeSendTransactionCallbackDropEvent( + SentryTransaction event, +) => + null; + FutureOr asyncBeforeSendCallbackDropEvent( SentryEvent event, { Hint? hint, @@ -1447,6 +1520,12 @@ FutureOr asyncBeforeSendCallbackDropEvent( return null; } +FutureOr asyncBeforeSendTransactionCallbackDropEvent( + SentryEvent event) async { + await Future.delayed(Duration(milliseconds: 200)); + return null; +} + FutureOr beforeSendCallback(SentryEvent event, {Hint? hint}) { return event ..tags!.addAll({'theme': 'material'}) @@ -1458,6 +1537,16 @@ FutureOr beforeSendCallback(SentryEvent event, {Hint? hint}) { ..sdk!.addPackage('test-pkg', '1.0'); } +FutureOr beforeSendTransactionCallback( + SentryTransaction transaction) { + return transaction + ..tags!.addAll({'theme': 'material'}) + ..extra!['host'] = '0.0.0.1' + ..sdk!.addIntegration('testIntegration') + ..sdk!.addPackage('test-pkg', '1.0') + ..breadcrumbs!.add(Breadcrumb(message: 'processor crumb')); +} + class Fixture { final recorder = MockClientReportRecorder(); final transport = MockTransport(); @@ -1477,6 +1566,7 @@ class Fixture { bool attachThreads = false, double? sampleRate, BeforeSendCallback? beforeSend, + BeforeSendTransactionCallback? beforeSendTransaction, EventProcessor? eventProcessor, bool provideMockRecorder = true, bool debug = false, @@ -1494,6 +1584,7 @@ class Fixture { options.attachThreads = attachThreads; options.sampleRate = sampleRate; options.beforeSend = beforeSend; + options.beforeSendTransaction = beforeSendTransaction; options.debug = debug; options.logger = mockLogger; @@ -1514,6 +1605,14 @@ class Fixture { return null; } + SentryTransaction fakeTransaction() { + return SentryTransaction( + tracer, + sdk: SdkVersion(name: 'sdk1', version: '1.0.0'), + breadcrumbs: [], + ); + } + void mockLogger( SentryLevel level, String message, {