Skip to content

Commit 21f1bf7

Browse files
authored
Breadcrumbs for file I/O operations (#1649)
1 parent cd16818 commit 21f1bf7

File tree

4 files changed

+146
-4
lines changed

4 files changed

+146
-4
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Breadcrumbs for file I/O operations ([#1649](https://github.com/getsentry/sentry-dart/pull/1649))
8+
59
### Dependencies
610

711
- Enable compatibility with uuid v4 ([#1647](https://github.com/getsentry/sentry-dart/pull/1647))

file/lib/src/sentry_file.dart

+30-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ import 'version.dart';
215215
typedef Callback<T> = FutureOr<T> Function();
216216

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

426427
span?.origin = SentryTraceOrigins.autoFile;
427428
span?.setData('file.async', true);
429+
430+
final Map<String, dynamic> breadcrumbData = {};
431+
breadcrumbData['file.async'] = true;
432+
428433
if (_hub.options.sendDefaultPii) {
429434
span?.setData('file.path', absolute.path);
435+
breadcrumbData['file.path'] = absolute.path;
430436
}
431437
T data;
432438
try {
@@ -453,6 +459,7 @@ class SentryFile implements File {
453459

454460
if (length != null) {
455461
span?.setData('file.size', length);
462+
breadcrumbData['file.size'] = length;
456463
}
457464

458465
span?.status = SpanStatus.ok();
@@ -462,6 +469,14 @@ class SentryFile implements File {
462469
rethrow;
463470
} finally {
464471
await span?.finish();
472+
473+
await _hub.addBreadcrumb(
474+
Breadcrumb(
475+
message: desc,
476+
data: breadcrumbData,
477+
category: operation,
478+
),
479+
);
465480
}
466481
return data;
467482
}
@@ -475,8 +490,12 @@ class SentryFile implements File {
475490
span?.origin = SentryTraceOrigins.autoFile;
476491
span?.setData('file.async', false);
477492

493+
final Map<String, dynamic> breadcrumbData = {};
494+
breadcrumbData['file.async'] = false;
495+
478496
if (_hub.options.sendDefaultPii) {
479497
span?.setData('file.path', absolute.path);
498+
breadcrumbData['file.path'] = absolute.path;
480499
}
481500

482501
T data;
@@ -504,6 +523,7 @@ class SentryFile implements File {
504523

505524
if (length != null) {
506525
span?.setData('file.size', length);
526+
breadcrumbData['file.size'] = length;
507527
}
508528

509529
span?.status = SpanStatus.ok();
@@ -513,6 +533,14 @@ class SentryFile implements File {
513533
rethrow;
514534
} finally {
515535
span?.finish();
536+
537+
_hub.addBreadcrumb(
538+
Breadcrumb(
539+
message: desc,
540+
data: breadcrumbData,
541+
category: operation,
542+
),
543+
);
516544
}
517545
return data;
518546
}

file/test/mock_sentry_client.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
1414
SentryTraceContextHeader? traceContext,
1515
}) async {
1616
captureTransactionCalls
17-
.add(CaptureTransactionCall(transaction, traceContext));
17+
.add(CaptureTransactionCall(transaction, traceContext, scope));
1818
return transaction.eventId;
1919
}
2020
}
2121

2222
class CaptureTransactionCall {
2323
final SentryTransaction transaction;
2424
final SentryTraceContextHeader? traceContext;
25+
final Scope? scope;
2526

26-
CaptureTransactionCall(this.transaction, this.traceContext);
27+
CaptureTransactionCall(this.transaction, this.traceContext, this.scope);
2728
}

file/test/sentry_file_test.dart

+109
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ void main() {
3636
expect(span.origin, SentryTraceOrigins.autoFile);
3737
}
3838

39+
void _asserBreadcrumb(bool async) {
40+
final call = fixture.client.captureTransactionCalls.first;
41+
final breadcrumb = call.scope?.breadcrumbs.first;
42+
43+
expect(breadcrumb?.category, 'file.copy');
44+
expect(breadcrumb?.data?['file.size'], 7);
45+
expect(breadcrumb?.data?['file.async'], async);
46+
expect(breadcrumb?.message, 'testfile.txt');
47+
expect(
48+
(breadcrumb?.data?['file.path'] as String)
49+
.endsWith('test_resources/testfile.txt'),
50+
true);
51+
}
52+
3953
test('async', () async {
4054
final file = File('test_resources/testfile.txt');
4155

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

5872
_assertSpan(true);
73+
_asserBreadcrumb(true);
5974

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

8297
_assertSpan(false);
98+
_asserBreadcrumb(false);
8399

84100
newFile.deleteSync();
85101
});
@@ -107,6 +123,20 @@ void main() {
107123
expect(span.origin, SentryTraceOrigins.autoFile);
108124
}
109125

126+
void _assertBreadcrumb(bool async, {int? size = 0}) {
127+
final call = fixture.client.captureTransactionCalls.first;
128+
final breadcrumb = call.scope?.breadcrumbs.first;
129+
130+
expect(breadcrumb?.category, 'file.write');
131+
expect(breadcrumb?.data?['file.size'], size);
132+
expect(breadcrumb?.data?['file.async'], async);
133+
expect(breadcrumb?.message, 'testfile_create.txt');
134+
expect(
135+
(breadcrumb?.data?['file.path'] as String)
136+
.endsWith('test_resources/testfile_create.txt'),
137+
true);
138+
}
139+
110140
test('async', () async {
111141
final file = File('test_resources/testfile_create.txt');
112142
expect(await file.exists(), false);
@@ -126,6 +156,7 @@ void main() {
126156
expect(await newFile.exists(), true);
127157

128158
_assertSpan(true);
159+
_assertBreadcrumb(true);
129160

130161
await newFile.delete();
131162
});
@@ -149,6 +180,7 @@ void main() {
149180
expect(sut.existsSync(), true);
150181

151182
_assertSpan(false);
183+
_assertBreadcrumb(false);
152184

153185
sut.deleteSync();
154186
});
@@ -176,6 +208,20 @@ void main() {
176208
expect(span.origin, SentryTraceOrigins.autoFile);
177209
}
178210

211+
void _assertBreadcrumb(bool async, {int? size = 0}) {
212+
final call = fixture.client.captureTransactionCalls.first;
213+
final breadcrumb = call.scope?.breadcrumbs.first;
214+
215+
expect(breadcrumb?.category, 'file.delete');
216+
expect(breadcrumb?.data?['file.size'], size);
217+
expect(breadcrumb?.data?['file.async'], async);
218+
expect(breadcrumb?.message, 'testfile_delete.txt');
219+
expect(
220+
(breadcrumb?.data?['file.path'] as String)
221+
.endsWith('test_resources/testfile_delete.txt'),
222+
true);
223+
}
224+
179225
test('async', () async {
180226
final file = File('test_resources/testfile_delete.txt');
181227
await file.create();
@@ -196,6 +242,7 @@ void main() {
196242
expect(await newFile.exists(), false);
197243

198244
_assertSpan(true);
245+
_assertBreadcrumb(true);
199246
});
200247

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

220267
_assertSpan(false);
268+
_assertBreadcrumb(false);
221269
});
222270
});
223271

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

294+
void _assertBreadcrumb() {
295+
final call = fixture.client.captureTransactionCalls.first;
296+
final breadcrumb = call.scope?.breadcrumbs.first;
297+
298+
expect(breadcrumb?.category, 'file.open');
299+
expect(breadcrumb?.data?['file.size'], 3535);
300+
expect(breadcrumb?.data?['file.async'], true);
301+
expect(breadcrumb?.message, 'sentry.png');
302+
expect(
303+
(breadcrumb?.data?['file.path'] as String)
304+
.endsWith('test_resources/sentry.png'),
305+
true);
306+
}
307+
246308
test('async', () async {
247309
final file = File('test_resources/sentry.png');
248310

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

263325
_assertSpan();
326+
_assertBreadcrumb();
264327
});
265328
});
266329

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

352+
void _assertBreadcrumb(String fileName, bool async, {int? size = 0}) {
353+
final call = fixture.client.captureTransactionCalls.first;
354+
final breadcrumb = call.scope?.breadcrumbs.first;
355+
356+
expect(breadcrumb?.category, 'file.read');
357+
expect(breadcrumb?.data?['file.size'], size);
358+
expect(breadcrumb?.data?['file.async'], async);
359+
expect(breadcrumb?.message, fileName);
360+
expect(
361+
(breadcrumb?.data?['file.path'] as String)
362+
.endsWith('test_resources/$fileName'),
363+
true);
364+
}
365+
289366
test('as bytes async', () async {
290367
final file = File('test_resources/sentry.png');
291368

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

304381
_assertSpan('sentry.png', true, size: 3535);
382+
_assertBreadcrumb('sentry.png', true, size: 3535);
305383
});
306384

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

322400
_assertSpan('sentry.png', false, size: 3535);
401+
_assertBreadcrumb('sentry.png', false, size: 3535);
323402
});
324403

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

340419
_assertSpan('testfile.txt', true, size: 7);
420+
_assertBreadcrumb('testfile.txt', true, size: 7);
341421
});
342422

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

358438
_assertSpan('testfile.txt', false, size: 7);
439+
_assertBreadcrumb('testfile.txt', false, size: 7);
359440
});
360441

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

376457
_assertSpan('testfile.txt', true, size: 7);
458+
_assertBreadcrumb('testfile.txt', true, size: 7);
377459
});
378460

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

394476
_assertSpan('testfile.txt', false, size: 7);
477+
_assertBreadcrumb('testfile.txt', false, size: 7);
395478
});
396479
});
397480

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

502+
void _assertBreadcrumb(bool async, String name) {
503+
final call = fixture.client.captureTransactionCalls.first;
504+
final breadcrumb = call.scope?.breadcrumbs.first;
505+
506+
expect(breadcrumb?.category, 'file.rename');
507+
expect(breadcrumb?.data?['file.size'], 0);
508+
expect(breadcrumb?.data?['file.async'], async);
509+
expect(breadcrumb?.message, name);
510+
expect(
511+
(breadcrumb?.data?['file.path'] as String)
512+
.endsWith('test_resources/$name'),
513+
true);
514+
}
515+
419516
test('async', () async {
420517
final file = File('test_resources/old_name.txt');
421518
await file.create();
@@ -438,6 +535,7 @@ void main() {
438535
expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath()));
439536

440537
_assertSpan(true, 'old_name.txt');
538+
_assertBreadcrumb(true, 'old_name.txt');
441539

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

466564
_assertSpan(false, 'old_name.txt');
565+
_assertBreadcrumb(false, 'old_name.txt');
467566

468567
newFile.deleteSync();
469568
});
@@ -485,6 +584,14 @@ void main() {
485584
expect(span.origin, SentryTraceOrigins.autoFile);
486585
}
487586

587+
void _assertBreadcrumb(bool async) {
588+
final call = fixture.client.captureTransactionCalls.first;
589+
final breadcrumb = call.scope?.breadcrumbs.first;
590+
591+
expect(breadcrumb?.data?['file.async'], async);
592+
expect(breadcrumb?.data?['file.path'], null);
593+
}
594+
488595
test('does not add file path if sendDefaultPii is disabled async',
489596
() async {
490597
final file = File('test_resources/testfile.txt');
@@ -501,6 +608,7 @@ void main() {
501608
await tr.finish();
502609

503610
_assertSpan(true);
611+
_assertBreadcrumb(true);
504612
});
505613

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

520628
_assertSpan(false);
629+
_assertBreadcrumb(false);
521630
});
522631

523632
test('add SentryFileTracing integration', () async {

0 commit comments

Comments
 (0)