Skip to content

Commit

Permalink
Support beforeSendTransaction (#1238)
Browse files Browse the repository at this point in the history
  • Loading branch information
denrase authored Feb 8, 2023
1 parent e6cb627 commit 231b60b
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 56 additions & 24 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class SentryClient {
return _sentryId;
}

preparedEvent = await _processEvent(
preparedEvent = await _runEventProcessors(
preparedEvent,
eventProcessors: _options.eventProcessors,
hint: hint,
Expand All @@ -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 &&
Expand Down Expand Up @@ -324,7 +311,7 @@ class SentryClient {
return _sentryId;
}

preparedTransaction = await _processEvent(
preparedTransaction = await _runEventProcessors(
preparedTransaction,
eventProcessors: _options.eventProcessors,
) as SentryTransaction?;
Expand All @@ -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();
Expand Down Expand Up @@ -366,7 +361,44 @@ class SentryClient {

void close() => _options.httpClient.close();

Future<SentryEvent?> _processEvent(
Future<SentryEvent?> _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<SentryEvent?> _runEventProcessors(
SentryEvent event, {
Hint? hint,
required List<EventProcessor> eventProcessors,
Expand Down
10 changes: 10 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -425,6 +429,12 @@ typedef BeforeSendCallback = FutureOr<SentryEvent?> 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<SentryTransaction?> 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(
Expand Down
99 changes: 99 additions & 0 deletions dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -1439,6 +1507,11 @@ FutureOr<SentryEvent?> beforeSendCallbackDropEvent(
}) =>
null;

FutureOr<SentryTransaction?> beforeSendTransactionCallbackDropEvent(
SentryTransaction event,
) =>
null;

FutureOr<SentryEvent?> asyncBeforeSendCallbackDropEvent(
SentryEvent event, {
Hint? hint,
Expand All @@ -1447,6 +1520,12 @@ FutureOr<SentryEvent?> asyncBeforeSendCallbackDropEvent(
return null;
}

FutureOr<SentryTransaction?> asyncBeforeSendTransactionCallbackDropEvent(
SentryEvent event) async {
await Future.delayed(Duration(milliseconds: 200));
return null;
}

FutureOr<SentryEvent?> beforeSendCallback(SentryEvent event, {Hint? hint}) {
return event
..tags!.addAll({'theme': 'material'})
Expand All @@ -1458,6 +1537,16 @@ FutureOr<SentryEvent?> beforeSendCallback(SentryEvent event, {Hint? hint}) {
..sdk!.addPackage('test-pkg', '1.0');
}

FutureOr<SentryTransaction?> 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();
Expand All @@ -1477,6 +1566,7 @@ class Fixture {
bool attachThreads = false,
double? sampleRate,
BeforeSendCallback? beforeSend,
BeforeSendTransactionCallback? beforeSendTransaction,
EventProcessor? eventProcessor,
bool provideMockRecorder = true,
bool debug = false,
Expand All @@ -1494,6 +1584,7 @@ class Fixture {
options.attachThreads = attachThreads;
options.sampleRate = sampleRate;
options.beforeSend = beforeSend;
options.beforeSendTransaction = beforeSendTransaction;
options.debug = debug;
options.logger = mockLogger;

Expand All @@ -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, {
Expand Down

0 comments on commit 231b60b

Please sign in to comment.