From 21f1bf76b7669a57e83a66238fce9dfe1f79c57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 25 Sep 2023 10:44:55 +0000 Subject: [PATCH] Breadcrumbs for file I/O operations (#1649) --- CHANGELOG.md | 4 ++ file/lib/src/sentry_file.dart | 32 ++++++++- file/test/mock_sentry_client.dart | 5 +- file/test/sentry_file_test.dart | 109 ++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3e79e483..40d3b04f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/file/lib/src/sentry_file.dart b/file/lib/src/sentry_file.dart index 4b7659dc0c..de0004c938 100644 --- a/file/lib/src/sentry_file.dart +++ b/file/lib/src/sentry_file.dart @@ -215,7 +215,8 @@ import 'version.dart'; typedef Callback = FutureOr 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. /// @@ -228,7 +229,7 @@ typedef Callback = FutureOr 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 @@ -425,8 +426,13 @@ class SentryFile implements File { span?.origin = SentryTraceOrigins.autoFile; span?.setData('file.async', true); + + final Map breadcrumbData = {}; + breadcrumbData['file.async'] = true; + if (_hub.options.sendDefaultPii) { span?.setData('file.path', absolute.path); + breadcrumbData['file.path'] = absolute.path; } T data; try { @@ -453,6 +459,7 @@ class SentryFile implements File { if (length != null) { span?.setData('file.size', length); + breadcrumbData['file.size'] = length; } span?.status = SpanStatus.ok(); @@ -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; } @@ -475,8 +490,12 @@ class SentryFile implements File { span?.origin = SentryTraceOrigins.autoFile; span?.setData('file.async', false); + final Map breadcrumbData = {}; + breadcrumbData['file.async'] = false; + if (_hub.options.sendDefaultPii) { span?.setData('file.path', absolute.path); + breadcrumbData['file.path'] = absolute.path; } T data; @@ -504,6 +523,7 @@ class SentryFile implements File { if (length != null) { span?.setData('file.size', length); + breadcrumbData['file.size'] = length; } span?.status = SpanStatus.ok(); @@ -513,6 +533,14 @@ class SentryFile implements File { rethrow; } finally { span?.finish(); + + _hub.addBreadcrumb( + Breadcrumb( + message: desc, + data: breadcrumbData, + category: operation, + ), + ); } return data; } diff --git a/file/test/mock_sentry_client.dart b/file/test/mock_sentry_client.dart index c68fde9b47..4a4a28142d 100644 --- a/file/test/mock_sentry_client.dart +++ b/file/test/mock_sentry_client.dart @@ -14,7 +14,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { SentryTraceContextHeader? traceContext, }) async { captureTransactionCalls - .add(CaptureTransactionCall(transaction, traceContext)); + .add(CaptureTransactionCall(transaction, traceContext, scope)); return transaction.eventId; } } @@ -22,6 +22,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { class CaptureTransactionCall { final SentryTransaction transaction; final SentryTraceContextHeader? traceContext; + final Scope? scope; - CaptureTransactionCall(this.transaction, this.traceContext); + CaptureTransactionCall(this.transaction, this.traceContext, this.scope); } diff --git a/file/test/sentry_file_test.dart b/file/test/sentry_file_test.dart index a3fc47f670..3f7135de54 100644 --- a/file/test/sentry_file_test.dart +++ b/file/test/sentry_file_test.dart @@ -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'); @@ -56,6 +70,7 @@ void main() { expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); _assertSpan(true); + _asserBreadcrumb(true); await newFile.delete(); }); @@ -80,6 +95,7 @@ void main() { expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); _assertSpan(false); + _asserBreadcrumb(false); newFile.deleteSync(); }); @@ -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); @@ -126,6 +156,7 @@ void main() { expect(await newFile.exists(), true); _assertSpan(true); + _assertBreadcrumb(true); await newFile.delete(); }); @@ -149,6 +180,7 @@ void main() { expect(sut.existsSync(), true); _assertSpan(false); + _assertBreadcrumb(false); sut.deleteSync(); }); @@ -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(); @@ -196,6 +242,7 @@ void main() { expect(await newFile.exists(), false); _assertSpan(true); + _assertBreadcrumb(true); }); test('sync', () async { @@ -218,6 +265,7 @@ void main() { expect(sut.existsSync(), false); _assertSpan(false); + _assertBreadcrumb(false); }); }); @@ -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'); @@ -261,6 +323,7 @@ void main() { await newFile.close(); _assertSpan(); + _assertBreadcrumb(); }); }); @@ -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'); @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -392,6 +474,7 @@ void main() { await tr.finish(); _assertSpan('testfile.txt', false, size: 7); + _assertBreadcrumb('testfile.txt', false, size: 7); }); }); @@ -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(); @@ -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(); }); @@ -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(); }); @@ -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'); @@ -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 { @@ -518,6 +626,7 @@ void main() { await tr.finish(); _assertSpan(false); + _assertBreadcrumb(false); }); test('add SentryFileTracing integration', () async {