Skip to content

Breadcrumbs for file I/O operations #1649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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