From 955541a50762f04c301c83eb25aef0077742c29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 31 Oct 2023 17:15:16 +0000 Subject: [PATCH] Breadcrumbs for database operations (#1656) --- .github/workflows/flutter_test.yml | 2 +- CHANGELOG.md | 6 + flutter/example/ios/Podfile | 3 + sqflite/lib/src/sentry_batch.dart | 31 ++ sqflite/lib/src/sentry_database.dart | 36 +- sqflite/lib/src/sentry_database_executor.dart | 165 +++++++ sqflite/lib/src/sentry_sqflite.dart | 17 +- .../src/sentry_sqflite_database_factory.dart | 18 +- .../sentry_database_span_attributes.dart | 9 + sqflite/test/mocks/mocks.mocks.dart | 129 +++-- sqflite/test/sentry_batch_test.dart | 295 +++++++++++ sqflite/test/sentry_database_test.dart | 463 ++++++++++++++++++ ...ry_sqflite_database_factory_dart_test.dart | 16 + sqflite/test/sentry_sqflite_test.dart | 15 + 14 files changed, 1163 insertions(+), 42 deletions(-) diff --git a/.github/workflows/flutter_test.yml b/.github/workflows/flutter_test.yml index d0875a8604..28f3777fc7 100644 --- a/.github/workflows/flutter_test.yml +++ b/.github/workflows/flutter_test.yml @@ -141,7 +141,7 @@ jobs: run: | case "${{ matrix.target }}" in ios) - device=$(xcrun simctl create sentryPhone com.apple.CoreSimulator.SimDeviceType.iPhone-14 com.apple.CoreSimulator.SimRuntime.iOS-16-4) + device=$(xcrun simctl create sentryPhone com.apple.CoreSimulator.SimDeviceType.iPhone-14 com.apple.CoreSimulator.SimRuntime.iOS-17-0) xcrun simctl boot ${device} echo "platform=iOS Simulator,id=${device}" >> "$GITHUB_OUTPUT" ;; diff --git a/CHANGELOG.md b/CHANGELOG.md index 41975567fa..5f24a7ed7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Breadcrumbs for database operations ([#1656](https://github.com/getsentry/sentry-dart/pull/1656)) + ## 7.12.0 ### Enhancements diff --git a/flutter/example/ios/Podfile b/flutter/example/ios/Podfile index 7b1e6a2394..8d38fc9608 100644 --- a/flutter/example/ios/Podfile +++ b/flutter/example/ios/Podfile @@ -43,5 +43,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end end end diff --git a/sqflite/lib/src/sentry_batch.dart b/sqflite/lib/src/sentry_batch.dart index 150b1c51da..a510ad186f 100644 --- a/sqflite/lib/src/sentry_batch.dart +++ b/sqflite/lib/src/sentry_batch.dart @@ -56,6 +56,13 @@ class SentryBatch implements Batch { span?.origin = SentryTraceOrigins.autoDbSqfliteBatch; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: _buffer.toString().trim(), + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _batch.apply( noResult: noResult, @@ -64,14 +71,23 @@ class SentryBatch implements Batch { span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; + return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); + rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -94,6 +110,13 @@ class SentryBatch implements Batch { span?.origin = SentryTraceOrigins.autoDbSqfliteBatch; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: _buffer.toString().trim(), + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _batch.commit( exclusive: exclusive, @@ -102,15 +125,23 @@ class SentryBatch implements Batch { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); + rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } diff --git a/sqflite/lib/src/sentry_database.dart b/sqflite/lib/src/sentry_database.dart index d148eb7ded..fafcad06d9 100644 --- a/sqflite/lib/src/sentry_database.dart +++ b/sqflite/lib/src/sentry_database.dart @@ -75,24 +75,39 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { Future close() { return Future(() async { final currentSpan = _hub.getSpan(); + final description = 'Close DB: ${_database.path}'; final span = currentSpan?.startChild( dbOp, - description: 'Close DB: ${_database.path}', + description: description, ); // ignore: invalid_use_of_internal_member span?.origin = SentryTraceOrigins.autoDbSqfliteDatabase; + var breadcrumb = Breadcrumb( + message: description, + category: dbOp, + data: {}, + type: 'query', + ); + try { await _database.close(); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -126,14 +141,23 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { }) { return Future(() async { final currentSpan = _hub.getSpan(); + final description = 'Transaction DB: ${_database.path}'; final span = currentSpan?.startChild( _dbSqlOp, - description: 'Transaction DB: ${_database.path}', + description: description, ); // ignore: invalid_use_of_internal_member span?.origin = SentryTraceOrigins.autoDbSqfliteDatabase; setDatabaseAttributeData(span, dbName); + var breadcrumb = Breadcrumb( + message: description, + category: _dbSqlOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, dbName); + Future newAction(Transaction txn) async { final executor = SentryDatabaseExecutor( txn, @@ -152,15 +176,23 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { await _database.transaction(newAction, exclusive: exclusive); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } diff --git a/sqflite/lib/src/sentry_database_executor.dart b/sqflite/lib/src/sentry_database_executor.dart index c232ca9e24..f13b0eb261 100644 --- a/sqflite/lib/src/sentry_database_executor.dart +++ b/sqflite/lib/src/sentry_database_executor.dart @@ -47,20 +47,35 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: builder.sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.delete(table, where: where, whereArgs: whereArgs); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -78,17 +93,32 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { await _executor.execute(sql, arguments); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -116,6 +146,14 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: builder.sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.insert( table, @@ -125,15 +163,22 @@ class SentryDatabaseExecutor implements DatabaseExecutor { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -173,6 +218,14 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: builder.sql, + category: SentryDatabase.dbSqlQueryOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.query( table, @@ -188,15 +241,22 @@ class SentryDatabaseExecutor implements DatabaseExecutor { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -237,6 +297,14 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: builder.sql, + category: SentryDatabase.dbSqlQueryOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.queryCursor( table, @@ -253,15 +321,22 @@ class SentryDatabaseExecutor implements DatabaseExecutor { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -278,19 +353,34 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.rawDelete(sql, arguments); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -307,19 +397,34 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.rawInsert(sql, arguments); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -339,19 +444,34 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlQueryOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.rawQuery(sql, arguments); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -372,6 +492,14 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlQueryOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.rawQueryCursor( sql, @@ -380,15 +508,22 @@ class SentryDatabaseExecutor implements DatabaseExecutor { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -405,19 +540,34 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.rawUpdate(sql, arguments); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } @@ -447,6 +597,14 @@ class SentryDatabaseExecutor implements DatabaseExecutor { span?.origin = SentryTraceOrigins.autoDbSqfliteDatabaseExecutor; setDatabaseAttributeData(span, _dbName); + var breadcrumb = Breadcrumb( + message: builder.sql, + category: SentryDatabase.dbSqlExecuteOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, _dbName); + try { final result = await _executor.update( table, @@ -457,15 +615,22 @@ class SentryDatabaseExecutor implements DatabaseExecutor { ); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; return result; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } diff --git a/sqflite/lib/src/sentry_sqflite.dart b/sqflite/lib/src/sentry_sqflite.dart index ebd39b547b..b26b767808 100644 --- a/sqflite/lib/src/sentry_sqflite.dart +++ b/sqflite/lib/src/sentry_sqflite.dart @@ -40,13 +40,20 @@ Future openDatabaseWithSentry( final newHub = hub ?? HubAdapter(); final currentSpan = newHub.getSpan(); + final description = 'Open DB: $path'; final span = currentSpan?.startChild( SentryDatabase.dbOp, - description: 'Open DB: $path', + description: description, ); // ignore: invalid_use_of_internal_member span?.origin = SentryTraceOrigins.autoDbSqfliteOpenDatabase; + var breadcrumb = Breadcrumb( + message: description, + category: SentryDatabase.dbOp, + data: {}, + ); + try { final database = await databaseFactory.openDatabase(path, options: dbOptions); @@ -54,14 +61,22 @@ Future openDatabaseWithSentry( final sentryDatabase = SentryDatabase(database, hub: newHub); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; + return sentryDatabase; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await newHub.scope.addBreadcrumb(breadcrumb); } }); } diff --git a/sqflite/lib/src/sentry_sqflite_database_factory.dart b/sqflite/lib/src/sentry_sqflite_database_factory.dart index 4fff7479b1..827e516afa 100644 --- a/sqflite/lib/src/sentry_sqflite_database_factory.dart +++ b/sqflite/lib/src/sentry_sqflite_database_factory.dart @@ -59,15 +59,22 @@ class SentrySqfliteDatabaseFactory with SqfliteDatabaseFactoryMixin { return Future(() async { final currentSpan = _hub.getSpan(); + final description = 'Open DB: $path'; final span = currentSpan?.startChild( SentryDatabase.dbOp, - description: 'Open DB: $path', + description: description, ); span?.origin = // ignore: invalid_use_of_internal_member SentryTraceOrigins.autoDbSqfliteDatabaseFactory; + var breadcrumb = Breadcrumb( + message: description, + category: SentryDatabase.dbOp, + data: {}, + ); + try { final database = await databaseFactory.openDatabase(path, options: options); @@ -75,14 +82,21 @@ class SentrySqfliteDatabaseFactory with SqfliteDatabaseFactoryMixin { final sentryDatabase = SentryDatabase(database, hub: _hub); span?.status = SpanStatus.ok(); + breadcrumb.data?['status'] = 'ok'; + return sentryDatabase; } catch (exception) { span?.throwable = exception; span?.status = SpanStatus.internalError(); - + breadcrumb.data?['status'] = 'internal_error'; + breadcrumb = breadcrumb.copyWith( + level: SentryLevel.warning, + ); rethrow; } finally { await span?.finish(); + // ignore: invalid_use_of_internal_member + await _hub.scope.addBreadcrumb(breadcrumb); } }); } diff --git a/sqflite/lib/src/utils/sentry_database_span_attributes.dart b/sqflite/lib/src/utils/sentry_database_span_attributes.dart index 119f2dd1b8..347c750ee8 100644 --- a/sqflite/lib/src/utils/sentry_database_span_attributes.dart +++ b/sqflite/lib/src/utils/sentry_database_span_attributes.dart @@ -10,3 +10,12 @@ void setDatabaseAttributeData(ISentrySpan? span, String? dbName) { span?.setData(SentryDatabase.dbNameKey, dbName); } } + +/// Sets the database attributes on the [breadcrumb]. +/// It contains the database system and the database name. +void setDatabaseAttributeOnBreadcrumb(Breadcrumb breadcrumb, String? dbName) { + breadcrumb.data?[SentryDatabase.dbSystemKey] = SentryDatabase.dbSystem; + if (dbName != null) { + breadcrumb.data?[SentryDatabase.dbNameKey] = dbName; + } +} diff --git a/sqflite/test/mocks/mocks.mocks.dart b/sqflite/test/mocks/mocks.mocks.dart index 582663e5f9..6c4c5e362e 100644 --- a/sqflite/test/mocks/mocks.mocks.dart +++ b/sqflite/test/mocks/mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.2 from annotations // in sentry_sqflite/test/mocks/mocks.dart. // Do not manually edit this file. @@ -6,12 +6,13 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; import 'package:sentry/sentry.dart' as _i2; import 'package:sentry/src/sentry_tracer.dart' as _i5; import 'package:sqflite_common/sql.dart' as _i6; import 'package:sqflite_common/sqlite_api.dart' as _i3; -import 'mocks.dart' as _i7; +import 'mocks.dart' as _i8; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -76,7 +77,7 @@ class _FakeDatabase_4 extends _i1.SmartFake implements _i3.Database { ); } -class _FakeFuture_5 extends _i1.SmartFake implements _i4.Future { +class _FakeFuture_5 extends _i1.SmartFake implements _i4.Future { _FakeFuture_5( Object parent, Invocation parentInvocation, @@ -126,8 +127,18 @@ class _FakeSentryId_9 extends _i1.SmartFake implements _i2.SentryId { ); } -class _FakeHub_10 extends _i1.SmartFake implements _i2.Hub { - _FakeHub_10( +class _FakeScope_10 extends _i1.SmartFake implements _i2.Scope { + _FakeScope_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHub_11 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_11( Object parent, Invocation parentInvocation, ) : super( @@ -666,14 +677,25 @@ class MockDatabase extends _i1.Mock implements _i3.Database { [action], {#exclusive: exclusive}, ), - returnValue: _FakeFuture_5( - this, - Invocation.method( - #transaction, - [action], - {#exclusive: exclusive}, - ), - ), + returnValue: _i7.ifNotNull( + _i7.dummyValueOrNull( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), + (T v) => _i4.Future.value(v), + ) ?? + _FakeFuture_5( + this, + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + ), ) as _i4.Future); @override _i4.Future devInvokeMethod( @@ -688,16 +710,29 @@ class MockDatabase extends _i1.Mock implements _i3.Database { arguments, ], ), - returnValue: _FakeFuture_5( - this, - Invocation.method( - #devInvokeMethod, - [ - method, - arguments, - ], - ), - ), + returnValue: _i7.ifNotNull( + _i7.dummyValueOrNull( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), + (T v) => _i4.Future.value(v), + ) ?? + _FakeFuture_5( + this, + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + ), ) as _i4.Future); @override _i4.Future devInvokeSqlMethod( @@ -714,17 +749,31 @@ class MockDatabase extends _i1.Mock implements _i3.Database { arguments, ], ), - returnValue: _FakeFuture_5( - this, - Invocation.method( - #devInvokeSqlMethod, - [ - method, - sql, - arguments, - ], - ), - ), + returnValue: _i7.ifNotNull( + _i7.dummyValueOrNull( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), + (T v) => _i4.Future.value(v), + ) ?? + _FakeFuture_5( + this, + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + ), ) as _i4.Future); @override _i4.Future execute( @@ -1300,6 +1349,14 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ) as _i2.SentryId); @override + _i2.Scope get scope => (super.noSuchMethod( + Invocation.getter(#scope), + returnValue: _FakeScope_10( + this, + Invocation.getter(#scope), + ), + ) as _i2.Scope); + @override _i4.Future<_i2.SentryId> captureEvent( _i2.SentryEvent? event, { dynamic stackTrace, @@ -1433,7 +1490,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #clone, [], ), - returnValue: _FakeHub_10( + returnValue: _FakeHub_11( this, Invocation.method( #clone, @@ -1487,7 +1544,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i7.startTransactionShim( + returnValue: _i8.startTransactionShim( name, operation, description: description, diff --git a/sqflite/test/sentry_batch_test.dart b/sqflite/test/sentry_batch_test.dart index e3b24d811a..b9c53b8762 100644 --- a/sqflite/test/sentry_batch_test.dart +++ b/sqflite/test/sentry_batch_test.dart @@ -22,6 +22,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -63,6 +64,9 @@ void main() { // ignore: invalid_use_of_internal_member expect(span.origin, SentryTraceOrigins.autoDbSqfliteBatch); + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.data?['status'], 'ok'); + await db.close(); }); @@ -79,6 +83,9 @@ void main() { // ignore: invalid_use_of_internal_member expect(span.origin, SentryTraceOrigins.autoDbSqfliteBatch); + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.data?['status'], 'ok'); + await db.close(); }); @@ -102,6 +109,24 @@ void main() { await db.close(); }); + test('creates insert breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw insert span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -122,6 +147,24 @@ void main() { await db.close(); }); + test('creates raw insert breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawInsert('INSERT INTO Product (title) VALUES (?)', ['Product 1']); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates update span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -139,6 +182,24 @@ void main() { await db.close(); }); + test('creates update breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.update('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'UPDATE Product SET title = ?', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw update span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -156,6 +217,24 @@ void main() { await db.close(); }); + test('creates raw update breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawUpdate('UPDATE Product SET title = ?', ['Product 1']); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'UPDATE Product SET title = ?', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates delete span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -173,6 +252,24 @@ void main() { await db.close(); }); + test('creates delete breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.delete('Product'); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'DELETE FROM Product', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw delete span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -190,6 +287,24 @@ void main() { await db.close(); }); + test('creates raw delete breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawDelete('DELETE FROM Product'); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'DELETE FROM Product', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates execute span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -207,6 +322,24 @@ void main() { await db.close(); }); + test('creates execute breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.execute('DELETE FROM Product'); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'DELETE FROM Product', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates query span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -224,6 +357,24 @@ void main() { await db.close(); }); + test('creates query breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.query('Product'); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'SELECT * FROM Product', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw query span', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -241,6 +392,24 @@ void main() { await db.close(); }); + test('creates raw query breadcrumb', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawQuery('SELECT * FROM Product'); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.message, + 'SELECT * FROM Product', + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates span with batch description', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -263,6 +432,25 @@ SELECT * FROM Product'''; await db.close(); }); + test('creates breadcrumb with batch description', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + batch.query('Product'); + + await batch.commit(); + + final desc = '''INSERT INTO Product (title) VALUES (?) +SELECT * FROM Product'''; + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.message, desc); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates span with batch description using apply', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -285,6 +473,25 @@ SELECT * FROM Product'''; await db.close(); }); + test('creates breadcrumb with batch description using apply', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + batch.query('Product'); + + await batch.apply(); + + final desc = '''INSERT INTO Product (title) VALUES (?) +SELECT * FROM Product'''; + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.message, desc); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('apply creates db span with dbSystem and dbName attributes', () async { final db = await fixture.getDatabase(); final batch = db.batch(); @@ -300,6 +507,39 @@ SELECT * FROM Product'''; (db as SentryDatabase).dbName, ); + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect( + breadcrumb.data?[SentryDatabase.dbNameKey], + db.dbName, + ); + + await db.close(); + }); + + test('apply creates a breadcrumb with dbSystem and dbName attributes', + () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.apply(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect( + breadcrumb.data?[SentryDatabase.dbNameKey], + (db as SentryDatabase).dbName, + ); + expect(breadcrumb.type, 'query'); + await db.close(); }); @@ -322,6 +562,29 @@ SELECT * FROM Product'''; await db.close(); }); + test('commit creates breadcrumb with dbSystem and dbName attributes', + () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect( + breadcrumb.data?[SentryDatabase.dbNameKey], + (db as SentryDatabase).dbName, + ); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + tearDown(() { databaseFactory = sqfliteDatabaseFactoryDefault; }); @@ -334,6 +597,7 @@ SELECT * FROM Product'''; fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); }); @@ -353,6 +617,21 @@ SELECT * FROM Product'''; expect(span.origin, SentryTraceOrigins.autoDbSqfliteBatch); }); + test('commit sets batch to internal error if its thrown', () async { + final batch = SentryBatch(fixture.batch, hub: fixture.hub); + + when(fixture.batch.commit()).thenThrow(fixture.exception); + + batch.insert('Product', {'title': 'Product 1'}); + + await expectLater(() async => await batch.commit(), throwsException); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + test('apply sets span to internal error if its thrown', () async { final batch = SentryBatch(fixture.batch, hub: fixture.hub); @@ -368,6 +647,21 @@ SELECT * FROM Product'''; // ignore: invalid_use_of_internal_member expect(span.origin, SentryTraceOrigins.autoDbSqfliteBatch); }); + + test('apply sets breadcrumb to internal error if its thrown', () async { + final batch = SentryBatch(fixture.batch, hub: fixture.hub); + + when(fixture.batch.apply()).thenThrow(fixture.exception); + + batch.insert('Product', {'title': 'Product 1'}); + + await expectLater(() async => await batch.apply(), throwsException); + + final breadcrumb = fixture.hub.scope.breadcrumbs.last; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); }); } @@ -378,6 +672,7 @@ class Fixture { late final tracer = SentryTracer(_context, hub); final batch = MockBatch(); final exception = Exception('error'); + late final scope = Scope(options); Future getDatabase({ double? tracesSampleRate = 1.0, diff --git a/sqflite/test/sentry_database_test.dart b/sqflite/test/sentry_database_test.dart index 5c954426bb..ad1a07b360 100644 --- a/sqflite/test/sentry_database_test.dart +++ b/sqflite/test/sentry_database_test.dart @@ -26,6 +26,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -74,6 +75,17 @@ void main() { ); }); + test('creates close breadcrumb', () async { + final db = await fixture.getSut(); + + await db.close(); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.message, 'Close DB: $inMemoryDatabasePath'); + expect(breadcrumb.category, SentryDatabase.dbOp); + expect(breadcrumb.type, 'query'); + }); + test('creates transaction span', () async { final db = await fixture.getSut(); @@ -95,6 +107,27 @@ void main() { await db.close(); }); + test('creates transaction breadcrumb', () async { + final db = await fixture.getSut(); + + await db.transaction((txn) async { + expect(txn is SentrySqfliteTransaction, true); + }); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.message, 'Transaction DB: $inMemoryDatabasePath'); + expect(breadcrumb.category, 'db.sql.transaction'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates transaction children run by the transaction', () async { final db = await fixture.getSut(); @@ -177,6 +210,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); when(fixture.database.path).thenReturn('/path/db'); @@ -201,6 +235,19 @@ void main() { ); }); + test('close sets breadcrumb to internal error', () async { + when(fixture.database.close()).thenThrow(fixture.exception); + + final db = await fixture.getSut(database: fixture.database); + + await expectLater(() async => await db.close(), throwsException); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + test('transaction sets span to internal error', () async { // ignore: inference_failure_on_function_invocation when(fixture.database.transaction(any)).thenThrow(fixture.exception); @@ -222,6 +269,23 @@ void main() { SentryTraceOrigins.autoDbSqfliteDatabase, ); }); + + test('transaction sets breadcrumb to internal error', () async { + // ignore: inference_failure_on_function_invocation + when(fixture.database.transaction(any)).thenThrow(fixture.exception); + + final db = await fixture.getSut(database: fixture.database); + + await expectLater( + () async => await db.transaction((txn) async {}), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); }); group('$SentryDatabaseExecutor success', () { @@ -231,6 +295,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -259,6 +324,25 @@ void main() { await db.close(); }); + test('creates delete breadcrumb', () async { + final db = await fixture.getSut(); + + await db.delete('Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'DELETE FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates execute span', () async { final db = await fixture.getSut(); @@ -280,6 +364,25 @@ void main() { await db.close(); }); + test('creates execute breadcrumb', () async { + final db = await fixture.getSut(); + + await db.execute('DELETE FROM Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'DELETE FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates insert span', () async { final db = await fixture.getSut(); @@ -304,6 +407,28 @@ void main() { await db.close(); }); + test('creates insert breadcrumb', () async { + final db = await fixture.getSut(); + + await db.insert('Product', {'title': 'Product 1'}); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect( + breadcrumb.message, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates query span', () async { final db = await fixture.getSut(); @@ -325,6 +450,26 @@ void main() { await db.close(); }); + test('creates query breadcrumb', () async { + final db = await fixture.getSut(); + + await db.query('Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.query'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.message, 'SELECT * FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates query cursor span', () async { final db = await fixture.getSut(); @@ -346,6 +491,26 @@ void main() { await db.close(); }); + test('creates query cursor breadcrumb', () async { + final db = await fixture.getSut(); + + await db.queryCursor('Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.query'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.message, 'SELECT * FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw delete span', () async { final db = await fixture.getSut(); @@ -367,6 +532,25 @@ void main() { await db.close(); }); + test('creates raw delete breadcrumb', () async { + final db = await fixture.getSut(); + + await db.rawDelete('DELETE FROM Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'DELETE FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw insert span', () async { final db = await fixture.getSut(); @@ -392,6 +576,26 @@ void main() { await db.close(); }); + test('creates raw insert breadcrumb', () async { + final db = await fixture.getSut(); + + await db + .rawInsert('INSERT INTO Product (title) VALUES (?)', ['Product 1']); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'INSERT INTO Product (title) VALUES (?)'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw query span', () async { final db = await fixture.getSut(); @@ -413,6 +617,26 @@ void main() { await db.close(); }); + test('creates raw query breadcrumb', () async { + final db = await fixture.getSut(); + + await db.rawQuery('SELECT * FROM Product'); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.query'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.message, 'SELECT * FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw query cursor span', () async { final db = await fixture.getSut(); @@ -434,6 +658,26 @@ void main() { await db.close(); }); + test('creates raw query cursor breadcrumb', () async { + final db = await fixture.getSut(); + + await db.rawQueryCursor('SELECT * FROM Product', []); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.query'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.message, 'SELECT * FROM Product'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates raw update span', () async { final db = await fixture.getSut(); @@ -455,6 +699,25 @@ void main() { await db.close(); }); + test('creates raw update breadcrumb', () async { + final db = await fixture.getSut(); + + await db.rawUpdate('UPDATE Product SET title = ?', ['Product 1']); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'UPDATE Product SET title = ?'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + test('creates update span', () async { final db = await fixture.getSut(); @@ -476,6 +739,25 @@ void main() { await db.close(); }); + test('creates update breadcrumb', () async { + final db = await fixture.getSut(); + + await db.update('Product', {'title': 'Product 1'}); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db.sql.execute'); + expect(breadcrumb.message, 'UPDATE Product SET title = ?'); + expect(breadcrumb.data?['status'], 'ok'); + expect( + breadcrumb.data?[SentryDatabase.dbSystemKey], + SentryDatabase.dbSystem, + ); + expect(breadcrumb.data?[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect(breadcrumb.type, 'query'); + + await db.close(); + }); + tearDown(() { databaseFactory = sqfliteDatabaseFactoryDefault; }); @@ -488,6 +770,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); }); @@ -724,6 +1007,185 @@ void main() { SentryTraceOrigins.autoDbSqfliteDatabaseExecutor, ); }); + + test('delete sets breadcrumb to internal error', () async { + when(fixture.executor.delete(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.delete('Product'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('execute sets breadcrumb to internal error', () async { + when(fixture.executor.execute(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.execute('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('insert sets breadcrumb to internal error', () async { + when(fixture.executor.insert(any, any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor + .insert('Product', {'title': 'Product 1'}), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('query sets breadcrumb to internal error', () async { + when(fixture.executor.query(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.query('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('query cursor sets breadcrumb to internal error', () async { + when(fixture.executor.queryCursor(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.queryCursor('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('raw delete sets breadcrumb to internal error', () async { + when(fixture.executor.rawDelete(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.rawDelete('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('raw insert sets breadcrumb to internal error', () async { + when(fixture.executor.rawInsert(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.rawInsert('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('raw query sets breadcrumb to internal error', () async { + when(fixture.executor.rawQuery(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.rawQuery('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('raw query cursor sets breadcrumb to internal error', () async { + when(fixture.executor.rawQueryCursor(any, any)) + .thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.rawQueryCursor('sql', []), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('raw update sets breadcrumb to internal error', () async { + when(fixture.executor.rawUpdate(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor.rawUpdate('sql'), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); + + test('update sets breadcrumb to internal error', () async { + when(fixture.executor.update(any, any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + await expectLater( + () async => await executor + .update('Product', {'title': 'Product 1'}), + throwsException, + ); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.data?['status'], 'internal_error'); + expect(breadcrumb.type, 'query'); + expect(breadcrumb.level, SentryLevel.warning); + }); }); } @@ -735,6 +1197,7 @@ class Fixture { final database = MockDatabase(); final exception = Exception('error'); final executor = MockDatabaseExecutor(); + late final scope = Scope(options); Future getSut({ double? tracesSampleRate = 1.0, diff --git a/sqflite/test/sentry_sqflite_database_factory_dart_test.dart b/sqflite/test/sentry_sqflite_database_factory_dart_test.dart index d2ed2e9fb5..9eeac66734 100644 --- a/sqflite/test/sentry_sqflite_database_factory_dart_test.dart +++ b/sqflite/test/sentry_sqflite_database_factory_dart_test.dart @@ -24,6 +24,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -70,6 +71,20 @@ void main() { ); await db.close(); }); + + test('starts and finishes a open db breadcrumb when performance enabled', + () async { + final db = await openDatabase(inMemoryDatabasePath); + + expect((db as SentryDatabase).dbName, inMemoryDatabasePath); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, 'db'); + expect(breadcrumb.message, 'Open DB: $inMemoryDatabasePath'); + expect(breadcrumb.data?['status'], 'ok'); + + await db.close(); + }); }); tearDown(() { @@ -82,4 +97,5 @@ class Fixture { final options = SentryOptions(dsn: fakeDsn)..tracesSampleRate = 1.0; final _context = SentryTransactionContext('name', 'operation'); late final tracer = SentryTracer(_context, hub); + late final scope = Scope(options); } diff --git a/sqflite/test/sentry_sqflite_test.dart b/sqflite/test/sentry_sqflite_test.dart index ff75d67e6d..27851881b0 100644 --- a/sqflite/test/sentry_sqflite_test.dart +++ b/sqflite/test/sentry_sqflite_test.dart @@ -24,6 +24,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -68,6 +69,7 @@ void main() { fixture = Fixture(); when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.scope).thenReturn(fixture.scope); when(fixture.hub.getSpan()).thenReturn(fixture.tracer); // using ffi for testing on vm @@ -101,6 +103,18 @@ void main() { await db.close(); }); + + test('creates db open breadcrumb', () async { + final db = + await openDatabaseWithSentry(inMemoryDatabasePath, hub: fixture.hub); + + final breadcrumb = fixture.hub.scope.breadcrumbs.first; + expect(breadcrumb.category, SentryDatabase.dbOp); + expect(breadcrumb.message, 'Open DB: $inMemoryDatabasePath'); + expect(breadcrumb.data?['status'], 'ok'); + + await db.close(); + }); }); } @@ -109,4 +123,5 @@ class Fixture { final options = SentryOptions(dsn: fakeDsn)..tracesSampleRate = 1.0; final _context = SentryTransactionContext('name', 'operation'); late final tracer = SentryTracer(_context, hub); + late final scope = Scope(options); }