diff --git a/CHANGELOG.md b/CHANGELOG.md index f822c59ae8..fd7f354246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ - Add isar breadcrumbs ([#1800](https://github.com/getsentry/sentry-dart/pull/1800)) - Starting with Flutter 3.16, Sentry adds the [`appFlavor`](https://api.flutter.dev/flutter/services/appFlavor-constant.html) to the `flutter_context` ([#1799](https://github.com/getsentry/sentry-dart/pull/1799)) - Add beforeScreenshotCallback to SentryFlutterOptions ([#1805](https://github.com/getsentry/sentry-dart/pull/1805)) - +- Add support for `readTransaction` in `sqflite` ([#1819](https://github.com/getsentry/sentry-dart/pull/1819)) + ### Dependencies - Bump Android SDK from v7.0.0 to v7.1.0 ([#1788](https://github.com/getsentry/sentry-dart/pull/1788)) diff --git a/sqflite/lib/src/sentry_database.dart b/sqflite/lib/src/sentry_database.dart index fafcad06d9..23ac1dae63 100644 --- a/sqflite/lib/src/sentry_database.dart +++ b/sqflite/lib/src/sentry_database.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; import 'package:sqflite/sqflite.dart'; @@ -32,7 +34,10 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { // ignore: public_member_api_docs static const dbSqlQueryOp = 'db.sql.query'; - static const _dbSqlOp = 'db.sql.transaction'; + static const _dbSqlTransactionOp = 'db.sql.transaction'; + + static const _dbSqlReadTransactionOp = 'db.sql.read_transaction'; + @internal // ignore: public_member_api_docs static const dbSystemKey = 'db.system'; @@ -143,7 +148,7 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { final currentSpan = _hub.getSpan(); final description = 'Transaction DB: ${_database.path}'; final span = currentSpan?.startChild( - _dbSqlOp, + _dbSqlTransactionOp, description: description, ); // ignore: invalid_use_of_internal_member @@ -152,7 +157,7 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { var breadcrumb = Breadcrumb( message: description, - category: _dbSqlOp, + category: _dbSqlTransactionOp, data: {}, type: 'query', ); @@ -196,4 +201,86 @@ class SentryDatabase extends SentryDatabaseExecutor implements Database { } }); } + + @override + // ignore: override_on_non_overriding_member, public_member_api_docs + Future readTransaction(Future Function(Transaction txn) action) { + return Future(() async { + final currentSpan = _hub.getSpan(); + final description = 'Transaction DB: ${_database.path}'; + final span = currentSpan?.startChild( + _dbSqlReadTransactionOp, + description: description, + ); + // ignore: invalid_use_of_internal_member + span?.origin = SentryTraceOrigins.autoDbSqfliteDatabase; + setDatabaseAttributeData(span, dbName); + + var breadcrumb = Breadcrumb( + message: description, + category: _dbSqlReadTransactionOp, + data: {}, + type: 'query', + ); + setDatabaseAttributeOnBreadcrumb(breadcrumb, dbName); + + Future newAction(Transaction txn) async { + final executor = SentryDatabaseExecutor( + txn, + parentSpan: span, + hub: _hub, + dbName: dbName, + ); + final sentrySqfliteTransaction = + SentrySqfliteTransaction(executor, hub: _hub, dbName: dbName); + + return await action(sentrySqfliteTransaction); + } + + try { + final futureOrResult = _resolvedReadTransaction(newAction); + T result; + + if (futureOrResult is Future) { + result = await futureOrResult; + } else { + result = futureOrResult; + } + + 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); + } + }); + } + + FutureOr _resolvedReadTransaction( + Future Function(Transaction txn) action, + ) async { + try { + // ignore: return_of_invalid_type + final result = await (_database as dynamic).readTransaction(action); + // Await and cast, as directly returning the future resulted in a runtime error. + return result as T; + } on NoSuchMethodError catch (_) { + // The `readTransaction` does not exists on sqflite version < 2.5.0+2. + // Fallback to transaction instead. + return _database.transaction(action); + } + } } diff --git a/sqflite/test/sentry_database_test.dart b/sqflite/test/sentry_database_test.dart index ad1a07b360..492da311df 100644 --- a/sqflite/test/sentry_database_test.dart +++ b/sqflite/test/sentry_database_test.dart @@ -107,6 +107,27 @@ void main() { await db.close(); }); + test('creates readTransaction span', () async { + final db = await fixture.getSut(); + + await db.readTransaction((txn) async { + expect(txn is SentrySqfliteTransaction, true); + }); + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.read_transaction'); + expect(span.context.description, 'Transaction DB: $inMemoryDatabasePath'); + expect(span.status, SpanStatus.ok()); + expect(span.data[SentryDatabase.dbSystemKey], SentryDatabase.dbSystem); + expect(span.data[SentryDatabase.dbNameKey], inMemoryDatabasePath); + expect( + span.origin, + // ignore: invalid_use_of_internal_member + SentryTraceOrigins.autoDbSqfliteDatabase, + ); + + await db.close(); + }); + test('creates transaction breadcrumb', () async { final db = await fixture.getSut(); @@ -128,6 +149,27 @@ void main() { await db.close(); }); + test('creates readTransaction breadcrumb', () async { + final db = await fixture.getSut(); + + await db.readTransaction((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.read_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();