Skip to content

Commit

Permalink
Breadcrumbs for file I/O operations (#1649)
Browse files Browse the repository at this point in the history
  • Loading branch information
denrase authored Sep 25, 2023
1 parent cd16818 commit 21f1bf7
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Breadcrumbs for file I/O operations ([#1649](https://github.com/getsentry/sentry-dart/pull/1649))

### Dependencies

- Enable compatibility with uuid v4 ([#1647](https://github.com/getsentry/sentry-dart/pull/1647))
Expand Down
32 changes: 30 additions & 2 deletions file/lib/src/sentry_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ import 'version.dart';
typedef Callback<T> = FutureOr<T> Function();

/// The Sentry wrapper for the File IO implementation that creates a span
/// out of the active transaction in the scope.
/// out of the active transaction in the scope and a breadcrumb, which gets
/// added to the hub.
/// The span is started before the operation is executed and finished after.
/// The File tracing isn't available for Web.
///
Expand All @@ -228,7 +229,7 @@ typedef Callback<T> = FutureOr<T> Function();
/// final sentryFile = SentryFile(file);
/// // span starts
/// await sentryFile.writeAsString('Hello World');
/// // span finishes
/// // span finishes, adds breadcrumb
/// ```
///
/// All the copy, create, delete, open, rename, read, and write operations are
Expand Down Expand Up @@ -425,8 +426,13 @@ class SentryFile implements File {

span?.origin = SentryTraceOrigins.autoFile;
span?.setData('file.async', true);

final Map<String, dynamic> breadcrumbData = {};
breadcrumbData['file.async'] = true;

if (_hub.options.sendDefaultPii) {
span?.setData('file.path', absolute.path);
breadcrumbData['file.path'] = absolute.path;
}
T data;
try {
Expand All @@ -453,6 +459,7 @@ class SentryFile implements File {

if (length != null) {
span?.setData('file.size', length);
breadcrumbData['file.size'] = length;
}

span?.status = SpanStatus.ok();
Expand All @@ -462,6 +469,14 @@ class SentryFile implements File {
rethrow;
} finally {
await span?.finish();

await _hub.addBreadcrumb(
Breadcrumb(
message: desc,
data: breadcrumbData,
category: operation,
),
);
}
return data;
}
Expand All @@ -475,8 +490,12 @@ class SentryFile implements File {
span?.origin = SentryTraceOrigins.autoFile;
span?.setData('file.async', false);

final Map<String, dynamic> breadcrumbData = {};
breadcrumbData['file.async'] = false;

if (_hub.options.sendDefaultPii) {
span?.setData('file.path', absolute.path);
breadcrumbData['file.path'] = absolute.path;
}

T data;
Expand Down Expand Up @@ -504,6 +523,7 @@ class SentryFile implements File {

if (length != null) {
span?.setData('file.size', length);
breadcrumbData['file.size'] = length;
}

span?.status = SpanStatus.ok();
Expand All @@ -513,6 +533,14 @@ class SentryFile implements File {
rethrow;
} finally {
span?.finish();

_hub.addBreadcrumb(
Breadcrumb(
message: desc,
data: breadcrumbData,
category: operation,
),
);
}
return data;
}
Expand Down
5 changes: 3 additions & 2 deletions file/test/mock_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
SentryTraceContextHeader? traceContext,
}) async {
captureTransactionCalls
.add(CaptureTransactionCall(transaction, traceContext));
.add(CaptureTransactionCall(transaction, traceContext, scope));
return transaction.eventId;
}
}

class CaptureTransactionCall {
final SentryTransaction transaction;
final SentryTraceContextHeader? traceContext;
final Scope? scope;

CaptureTransactionCall(this.transaction, this.traceContext);
CaptureTransactionCall(this.transaction, this.traceContext, this.scope);
}
109 changes: 109 additions & 0 deletions file/test/sentry_file_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _asserBreadcrumb(bool async) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.copy');
expect(breadcrumb?.data?['file.size'], 7);
expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.message, 'testfile.txt');
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/testfile.txt'),
true);
}

test('async', () async {
final file = File('test_resources/testfile.txt');

Expand All @@ -56,6 +70,7 @@ void main() {
expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath()));

_assertSpan(true);
_asserBreadcrumb(true);

await newFile.delete();
});
Expand All @@ -80,6 +95,7 @@ void main() {
expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath()));

_assertSpan(false);
_asserBreadcrumb(false);

newFile.deleteSync();
});
Expand Down Expand Up @@ -107,6 +123,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb(bool async, {int? size = 0}) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.write');
expect(breadcrumb?.data?['file.size'], size);
expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.message, 'testfile_create.txt');
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/testfile_create.txt'),
true);
}

test('async', () async {
final file = File('test_resources/testfile_create.txt');
expect(await file.exists(), false);
Expand All @@ -126,6 +156,7 @@ void main() {
expect(await newFile.exists(), true);

_assertSpan(true);
_assertBreadcrumb(true);

await newFile.delete();
});
Expand All @@ -149,6 +180,7 @@ void main() {
expect(sut.existsSync(), true);

_assertSpan(false);
_assertBreadcrumb(false);

sut.deleteSync();
});
Expand Down Expand Up @@ -176,6 +208,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb(bool async, {int? size = 0}) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.delete');
expect(breadcrumb?.data?['file.size'], size);
expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.message, 'testfile_delete.txt');
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/testfile_delete.txt'),
true);
}

test('async', () async {
final file = File('test_resources/testfile_delete.txt');
await file.create();
Expand All @@ -196,6 +242,7 @@ void main() {
expect(await newFile.exists(), false);

_assertSpan(true);
_assertBreadcrumb(true);
});

test('sync', () async {
Expand All @@ -218,6 +265,7 @@ void main() {
expect(sut.existsSync(), false);

_assertSpan(false);
_assertBreadcrumb(false);
});
});

Expand All @@ -243,6 +291,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb() {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.open');
expect(breadcrumb?.data?['file.size'], 3535);
expect(breadcrumb?.data?['file.async'], true);
expect(breadcrumb?.message, 'sentry.png');
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/sentry.png'),
true);
}

test('async', () async {
final file = File('test_resources/sentry.png');

Expand All @@ -261,6 +323,7 @@ void main() {
await newFile.close();

_assertSpan();
_assertBreadcrumb();
});
});

Expand All @@ -286,6 +349,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb(String fileName, bool async, {int? size = 0}) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.read');
expect(breadcrumb?.data?['file.size'], size);
expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.message, fileName);
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/$fileName'),
true);
}

test('as bytes async', () async {
final file = File('test_resources/sentry.png');

Expand All @@ -302,6 +379,7 @@ void main() {
await tr.finish();

_assertSpan('sentry.png', true, size: 3535);
_assertBreadcrumb('sentry.png', true, size: 3535);
});

test('as bytes sync', () async {
Expand All @@ -320,6 +398,7 @@ void main() {
await tr.finish();

_assertSpan('sentry.png', false, size: 3535);
_assertBreadcrumb('sentry.png', false, size: 3535);
});

test('lines async', () async {
Expand All @@ -338,6 +417,7 @@ void main() {
await tr.finish();

_assertSpan('testfile.txt', true, size: 7);
_assertBreadcrumb('testfile.txt', true, size: 7);
});

test('lines sync', () async {
Expand All @@ -356,6 +436,7 @@ void main() {
await tr.finish();

_assertSpan('testfile.txt', false, size: 7);
_assertBreadcrumb('testfile.txt', false, size: 7);
});

test('string async', () async {
Expand All @@ -374,6 +455,7 @@ void main() {
await tr.finish();

_assertSpan('testfile.txt', true, size: 7);
_assertBreadcrumb('testfile.txt', true, size: 7);
});

test('string sync', () async {
Expand All @@ -392,6 +474,7 @@ void main() {
await tr.finish();

_assertSpan('testfile.txt', false, size: 7);
_assertBreadcrumb('testfile.txt', false, size: 7);
});
});

Expand All @@ -416,6 +499,20 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb(bool async, String name) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.category, 'file.rename');
expect(breadcrumb?.data?['file.size'], 0);
expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.message, name);
expect(
(breadcrumb?.data?['file.path'] as String)
.endsWith('test_resources/$name'),
true);
}

test('async', () async {
final file = File('test_resources/old_name.txt');
await file.create();
Expand All @@ -438,6 +535,7 @@ void main() {
expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath()));

_assertSpan(true, 'old_name.txt');
_assertBreadcrumb(true, 'old_name.txt');

await newFile.delete();
});
Expand All @@ -464,6 +562,7 @@ void main() {
expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath()));

_assertSpan(false, 'old_name.txt');
_assertBreadcrumb(false, 'old_name.txt');

newFile.deleteSync();
});
Expand All @@ -485,6 +584,14 @@ void main() {
expect(span.origin, SentryTraceOrigins.autoFile);
}

void _assertBreadcrumb(bool async) {
final call = fixture.client.captureTransactionCalls.first;
final breadcrumb = call.scope?.breadcrumbs.first;

expect(breadcrumb?.data?['file.async'], async);
expect(breadcrumb?.data?['file.path'], null);
}

test('does not add file path if sendDefaultPii is disabled async',
() async {
final file = File('test_resources/testfile.txt');
Expand All @@ -501,6 +608,7 @@ void main() {
await tr.finish();

_assertSpan(true);
_assertBreadcrumb(true);
});

test('does not add file path if sendDefaultPii is disabled sync', () async {
Expand All @@ -518,6 +626,7 @@ void main() {
await tr.finish();

_assertSpan(false);
_assertBreadcrumb(false);
});

test('add SentryFileTracing integration', () async {
Expand Down

0 comments on commit 21f1bf7

Please sign in to comment.