diff --git a/.craft.yml b/.craft.yml index 2c4c881eda..94d538b336 100644 --- a/.craft.yml +++ b/.craft.yml @@ -10,6 +10,7 @@ targets: logging: dio: file: + sqflite: - name: github - name: registry sdks: @@ -18,3 +19,4 @@ targets: pub:sentry_logging: pub:sentry_dio: pub:sentry_file: + pub:sentry_sqflite: diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 5654bd1dea..d400a7127d 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -8,6 +8,8 @@ on: paths-ignore: - 'logging/**' - 'dio/**' + - 'file/**' + - 'sqflite/**' jobs: cancel-previous-workflow: diff --git a/.github/workflows/dio.yml b/.github/workflows/dio.yml index 1d5d880f61..ccd8e4b5c9 100644 --- a/.github/workflows/dio.yml +++ b/.github/workflows/dio.yml @@ -8,6 +8,8 @@ on: paths-ignore: - 'logging/**' - 'flutter/**' + - 'file/**' + - 'sqflite/**' jobs: cancel-previous-workflow: diff --git a/.github/workflows/e2e_dart.yml b/.github/workflows/e2e_dart.yml index 4e16048886..340aca4543 100644 --- a/.github/workflows/e2e_dart.yml +++ b/.github/workflows/e2e_dart.yml @@ -9,6 +9,8 @@ on: - 'logging/**' - 'dio/**' - 'flutter/**' + - 'file/**' + - 'sqflite/**' env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/file.yml b/.github/workflows/file.yml index da046ac601..faa7a68956 100644 --- a/.github/workflows/file.yml +++ b/.github/workflows/file.yml @@ -9,6 +9,7 @@ on: - 'logging/**' - 'flutter/**' - 'dio/**' + - 'sqflite/**' jobs: cancel-previous-workflow: diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index f700d87768..7ebccce1b9 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -8,6 +8,8 @@ on: paths-ignore: - 'logging/**' - 'dio/**' + - 'file/**' + - 'sqflite/**' jobs: cancel-previous-workflow: @@ -62,9 +64,10 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 + if: ${{ matrix.target == 'android' }} with: + java-version: '11' distribution: 'adopt' - java-version: '8' # Install required dependencies for Flutter on Linux on Ubuntu - name: 'Setup Linux' diff --git a/.github/workflows/flutter_integration_test.yml b/.github/workflows/flutter_integration_test.yml index 359b3b2413..0d350e534d 100644 --- a/.github/workflows/flutter_integration_test.yml +++ b/.github/workflows/flutter_integration_test.yml @@ -3,7 +3,10 @@ on: push: branches: - main + - release/** pull_request: + paths-ignore: + - 'file/**' jobs: cancel-previous-workflow: @@ -29,7 +32,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'adopt' - java-version: '8' + java-version: '11' - uses: subosito/flutter-action@1e6ee87cb840500837bcd50a667fb28815d8e310 # pin@v2 with: diff --git a/.github/workflows/logging.yml b/.github/workflows/logging.yml index 438f2e0820..88b0bfcfe4 100644 --- a/.github/workflows/logging.yml +++ b/.github/workflows/logging.yml @@ -8,6 +8,8 @@ on: paths-ignore: - 'dio/**' - 'flutter/**' + - 'file/**' + - 'sqflite/**' jobs: cancel-previous-workflow: diff --git a/.github/workflows/min_version_test.yml b/.github/workflows/min_version_test.yml index 1c4ff09fe7..0c6a4270a0 100644 --- a/.github/workflows/min_version_test.yml +++ b/.github/workflows/min_version_test.yml @@ -5,6 +5,9 @@ on: - main - release/** pull_request: + paths-ignore: + - 'file/**' + - 'sqflite/**' jobs: cancel-previous-workflow: diff --git a/.github/workflows/sqflite.yml b/.github/workflows/sqflite.yml new file mode 100644 index 0000000000..b1894ab2e5 --- /dev/null +++ b/.github/workflows/sqflite.yml @@ -0,0 +1,114 @@ +name: sentry-sqflite +on: + push: + branches: + - main + - release/** + pull_request: + paths-ignore: + - 'logging/**' + - 'flutter/**' + - 'dio/**' + - 'file/**' + +jobs: + cancel-previous-workflow: + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@b173b6ec0100793626c2d9e6b90435061f4fc3e5 # pin@0.11.0 + with: + access_token: ${{ github.token }} + + build: + name: ${{ matrix.target }} | ${{ matrix.os }} | ${{ matrix.sdk }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + strategy: + fail-fast: false + # max-parallel: 4 + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + target: ['ios', 'android', 'macos', 'linux', 'windows'] + sdk: ['stable', 'beta'] + exclude: + - os: ubuntu-latest + target: ios + - os: ubuntu-latest + target: macos + - os: ubuntu-latest + target: windows + - os: windows-latest + target: ios + - os: windows-latest + target: macos + - os: windows-latest + target: linux + # macos-latest is taking hours due to limited resources + - os: macos-latest + target: android + - os: macos-latest + target: web + - os: macos-latest + target: linux + - os: macos-latest + target: windows + # Bad CPU type in executable + - os: macos-latest + sdk: beta + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + if: ${{ matrix.target == 'android' }} + with: + java-version: '11' + distribution: 'adopt' + + # Install required dependencies for Flutter on Linux on Ubuntu + - name: 'Setup Linux' + run: | + sudo apt update + sudo apt install -y cmake dbus libblkid-dev libgtk-3-dev liblzma-dev ninja-build pkg-config xvfb + sudo apt install -y network-manager upower + if: matrix.os == 'ubuntu-latest' + + - uses: subosito/flutter-action@dbf1fa04f4d2e52c33185153d06cdb5443aa189d # pin@v2 + with: + channel: ${{ matrix.sdk }} + + - run: flutter upgrade + + - name: Pub Get + run: | + cd sqflite + flutter pub get + + - name: Test VM with coverage + if: runner.os != 'macOS' + run: | + cd sqflite + flutter test --coverage --test-randomize-ordering-seed=random + + - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # pin@v3 + if: runner.os == 'Linux' && matrix.sdk == 'stable' && matrix.target == 'linux' + with: + name: sentry_sqflite + file: ./sqflite/coverage/lcov.info + functionalities: 'search' # remove after https://github.com/codecov/codecov-action/issues/600 + + - uses: VeryGoodOpenSource/very_good_coverage@84e5b54ab888644554e5573dca87d7f76dec9fb3 # pin@v2.0.0 + if: runner.os == 'Linux' && matrix.sdk == 'stable' && matrix.target == 'linux' + with: + path: './sqflite/coverage/lcov.info' + min_coverage: 80 + + analyze: + uses: ./.github/workflows/analyze.yml + with: + package: sqflite + sdk: flutter diff --git a/.gitignore b/.gitignore index 742ddcdedc..558e422972 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,11 @@ dart/coverage/* logging/coverage/* dio/coverage/* file/coverage/* +flutter/coverage/* +sqflite/coverage/* + pubspec.lock Podfile.lock -flutter/coverage/* .gradle flutter/.gradle diff --git a/CHANGELOG.md b/CHANGELOG.md index 513666f024..03726917e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- sqflite Support for Flutter ([#1306](https://github.com/getsentry/sentry-dart/pull/1306)) + ### Fixes - LoadImageListIntegration won't throw bad state if there is no exceptions in the event ([#1347](https://github.com/getsentry/sentry-dart/pull/1347)) diff --git a/README.md b/README.md index 443c0c1922..397f9f76bd 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Sentry SDK for Dart and Flutter | sentry_logging | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-logging/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Alogging) | [![pub package](https://img.shields.io/pub/v/sentry_logging.svg)](https://pub.dev/packages/sentry_logging) | [![likes](https://img.shields.io/pub/likes/sentry_logging?logo=dart)](https://pub.dev/packages/sentry_logging/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_logging?logo=dart)](https://pub.dev/packages/sentry_logging/score) | [![pub points](https://img.shields.io/pub/points/sentry_logging?logo=dart)](https://pub.dev/packages/sentry_logging/score) | sentry_dio | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-dio/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-dio) | [![pub package](https://img.shields.io/pub/v/sentry_dio.svg)](https://pub.dev/packages/sentry_dio) | [![likes](https://img.shields.io/pub/likes/sentry_dio?logo=dart)](https://pub.dev/packages/sentry_dio/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_dio?logo=dart)](https://pub.dev/packages/sentry_dio/score) | [![pub points](https://img.shields.io/pub/points/sentry_dio?logo=dart)](https://pub.dev/packages/sentry_dio/score) | sentry_file | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-file/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-file) | [![pub package](https://img.shields.io/pub/v/sentry_file.svg)](https://pub.dev/packages/sentry_file) | [![likes](https://img.shields.io/pub/likes/sentry_file?logo=dart)](https://pub.dev/packages/sentry_file/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_file?logo=dart)](https://pub.dev/packages/sentry_file/score) | [![pub points](https://img.shields.io/pub/points/sentry_file?logo=dart)](https://pub.dev/packages/sentry_file/score) +| sentry_sqflite | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-sqflite/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-sqflite) | [![pub package](https://img.shields.io/pub/v/sentry_sqflite.svg)](https://pub.dev/packages/sentry_sqflite) | [![likes](https://img.shields.io/pub/likes/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) | [![pub points](https://img.shields.io/pub/points/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) ##### Usage diff --git a/dart/lib/src/protocol/sdk_version.dart b/dart/lib/src/protocol/sdk_version.dart index f9f51ec404..e4b686a402 100644 --- a/dart/lib/src/protocol/sdk_version.dart +++ b/dart/lib/src/protocol/sdk_version.dart @@ -88,6 +88,12 @@ class SdkVersion { /// Adds a package void addPackage(String name, String version) { + for (final item in _packages) { + if (item.name == name && item.version == version) { + return; + } + } + final package = SentryPackage(name, version); _packages.add(package); } diff --git a/dart/test/protocol/sdk_version_test.dart b/dart/test/protocol/sdk_version_test.dart index 218ca72410..92f799712c 100644 --- a/dart/test/protocol/sdk_version_test.dart +++ b/dart/test/protocol/sdk_version_test.dart @@ -3,61 +3,46 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - final sdkVersion = SdkVersion( - name: 'name', - version: 'version', - integrations: ['test'], - packages: [SentryPackage('name', 'version')], - ); - - final sdkVersionJson = { - 'name': 'name', - 'version': 'version', - 'integrations': ['test'], - 'packages': [ - { - 'name': 'name', - 'version': 'version', - } - ], - }; - group('json', () { + final fixture = Fixture(); + test('toJson', () { - final json = sdkVersion.toJson(); + final json = fixture.getSut().toJson(); expect( - DeepCollectionEquality().equals(sdkVersionJson, json), + DeepCollectionEquality().equals(fixture.sdkVersionJson, json), true, ); }); test('fromJson', () { - final sdkVersion = SdkVersion.fromJson(sdkVersionJson); + final sdkVersion = SdkVersion.fromJson(fixture.sdkVersionJson); final json = sdkVersion.toJson(); expect( - DeepCollectionEquality().equals(sdkVersionJson, json), + DeepCollectionEquality().equals(fixture.sdkVersionJson, json), true, ); }); }); group('copyWith', () { + final fixture = Fixture(); + test('copyWith keeps unchanged', () { - final data = sdkVersion; + final sut = fixture.getSut(); - final copy = data.copyWith(); + final copy = sut.copyWith(); - expect(data.toJson(), copy.toJson()); + expect(sut.toJson(), copy.toJson()); }); test('copyWith takes new values', () { - final data = sdkVersion; + final sut = fixture.getSut(); final packages = [SentryPackage('name1', 'version1')]; final integrations = ['test1']; - final copy = data.copyWith( + final copy = sut.copyWith( name: 'name1', version: 'version1', integrations: integrations, @@ -76,4 +61,44 @@ void main() { expect('version1', copy.version); }); }); + + group('addPackage', () { + final fixture = Fixture(); + + test('add package if not same name and version', () { + final sut = fixture.getSut(); + sut.addPackage('name1', 'version1'); + + final last = sut.packages.last; + expect('name1', last.name); + expect('version1', last.version); + }); + test('does not add package if the same name and version', () { + final sut = fixture.getSut(); + sut.addPackage('name', 'version'); + + expect(1, sut.packages.length); + }); + }); +} + +class Fixture { + final sdkVersionJson = { + 'name': 'name', + 'version': 'version', + 'integrations': ['test'], + 'packages': [ + { + 'name': 'name', + 'version': 'version', + } + ], + }; + + SdkVersion getSut() => SdkVersion( + name: 'name', + version: 'version', + integrations: ['test'], + packages: [SentryPackage('name', 'version')], + ); } diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index d47cf18d49..69266beb80 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -507,7 +507,7 @@ void main() { updatedEvent?.contexts[SentryOperatingSystem.type].name, 'event-os'); }); - test('should apply the scope.contexts values ', () async { + test('should apply the scope.contexts values', () async { final event = SentryEvent(); final scope = Scope(SentryOptions(dsn: fakeDsn)); await scope.setContexts( diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 15b0cc421a..3839f28ffb 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -749,7 +749,7 @@ void main() { fixture = Fixture(); }); - test('should not apply the scope to non null event fields ', () async { + test('should not apply the scope to non null event fields', () async { final client = fixture.getSut(sendDefaultPii: true); final scope = await createScope(fixture.options); @@ -766,7 +766,7 @@ void main() { eventCrumbs.map((e) => e.toJson())); }); - test('should apply the scope user to null event user fields ', () async { + test('should apply the scope user to null event user fields', () async { final client = fixture.getSut(sendDefaultPii: true); final scope = await createScope(fixture.options); diff --git a/dio/lib/src/sentry_dio_extension.dart b/dio/lib/src/sentry_dio_extension.dart index 5d46e43eee..2bb77db3f0 100644 --- a/dio/lib/src/sentry_dio_extension.dart +++ b/dio/lib/src/sentry_dio_extension.dart @@ -6,6 +6,7 @@ import 'dio_stacktrace_extractor.dart'; import 'failed_request_interceptor.dart'; import 'sentry_transformer.dart'; import 'sentry_dio_client_adapter.dart'; +import 'version.dart'; /// Extension to add performance tracing for [Dio] extension SentryDioExtension on Dio { @@ -66,6 +67,7 @@ extension SentryDioExtension on Dio { options.sdk.addIntegration('sentry_dio'); options.addEventProcessor(DioEventProcessor(options)); } + options.sdk.addPackage(packageName, sdkVersion); if (options.captureFailedRequests) { // Add FailedRequestInterceptor at index 0, so it's the first interceptor. diff --git a/dio/lib/src/version.dart b/dio/lib/src/version.dart new file mode 100644 index 0000000000..e504d7ce28 --- /dev/null +++ b/dio/lib/src/version.dart @@ -0,0 +1,5 @@ +/// The SDK version reported to Sentry.io in the submitted events. +const String sdkVersion = '7.1.0'; + +/// The package name reported to Sentry.io in the submitted events. +const String packageName = 'pub:sentry_dio'; diff --git a/dio/pubspec.yaml b/dio/pubspec.yaml index f15b21b34b..975054c8a8 100644 --- a/dio/pubspec.yaml +++ b/dio/pubspec.yaml @@ -19,3 +19,4 @@ dev_dependencies: test: ^1.21.1 coverage: ^1.3.0 mockito: ^5.1.0 + yaml: ^3.1.0 # needed for version match (code and pubspec) diff --git a/dio/test/sentry_dio_extension_test.dart b/dio/test/sentry_dio_extension_test.dart index 8900d94ac6..2314deacca 100644 --- a/dio/test/sentry_dio_extension_test.dart +++ b/dio/test/sentry_dio_extension_test.dart @@ -5,6 +5,7 @@ import 'package:sentry_dio/src/dio_stacktrace_extractor.dart'; import 'package:sentry_dio/src/sentry_dio_client_adapter.dart'; import 'package:sentry_dio/src/sentry_dio_extension.dart'; import 'package:sentry_dio/src/sentry_transformer.dart'; +import 'package:sentry_dio/src/version.dart'; import 'package:test/test.dart'; import 'mocks/mock_hub.dart'; @@ -105,6 +106,19 @@ void main() { 1, ); }); + + test('addSentry adds package to sdk', () { + final dio = fixture.getSut(); + + dio.addSentry(hub: fixture.hub); + + expect( + fixture.hub.options.sdk.packages + .where((it) => it.name == packageName && it.version == sdkVersion) + .length, + 1, + ); + }); }); } diff --git a/dio/test/version_test.dart b/dio/test/version_test.dart new file mode 100644 index 0000000000..551d07a4c3 --- /dev/null +++ b/dio/test/version_test.dart @@ -0,0 +1,18 @@ +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry_dio/src/version.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart' as yaml; + +void main() { + test( + 'sdkVersion matches that of pubspec.yaml', + () { + final dynamic pubspec = + yaml.loadYaml(File('pubspec.yaml').readAsStringSync()); + expect(sdkVersion, pubspec['version']); + }, + ); +} diff --git a/file/lib/src/sentry_file.dart b/file/lib/src/sentry_file.dart index 195849dc2e..c357990538 100644 --- a/file/lib/src/sentry_file.dart +++ b/file/lib/src/sentry_file.dart @@ -9,6 +9,8 @@ import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; +import 'version.dart'; + typedef Callback = FutureOr Function(); /// The Sentry wrapper for the File IO implementation that creates a span @@ -36,6 +38,7 @@ class SentryFile implements File { @internal Hub? hub, }) : _hub = hub ?? HubAdapter() { _hub.options.sdk.addIntegration('SentryFileTracing'); + _hub.options.sdk.addPackage(packageName, sdkVersion); } final File _file; diff --git a/file/lib/src/version.dart b/file/lib/src/version.dart index 75cf106680..3634ceb880 100644 --- a/file/lib/src/version.dart +++ b/file/lib/src/version.dart @@ -1,2 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. const String sdkVersion = '7.1.0'; + +/// The package name reported to Sentry.io in the submitted events. +const String packageName = 'pub:sentry_file'; diff --git a/file/pubspec.yaml b/file/pubspec.yaml index 92524dc528..426046f7a6 100644 --- a/file/pubspec.yaml +++ b/file/pubspec.yaml @@ -18,3 +18,4 @@ dev_dependencies: test: ^1.21.1 coverage: ^1.3.0 mockito: ^5.1.0 + yaml: ^3.1.0 # needed for version match (code and pubspec) diff --git a/file/test/sentry_file_test.dart b/file/test/sentry_file_test.dart index 74b10ac8cc..62c5afb070 100644 --- a/file/test/sentry_file_test.dart +++ b/file/test/sentry_file_test.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; import 'package:sentry_file/sentry_file.dart'; +import 'package:sentry_file/src/version.dart'; import 'package:test/test.dart'; import 'mock_sentry_client.dart'; @@ -523,6 +524,22 @@ void main() { expect(fixture.hub.options.sdk.integrations.contains('SentryFileTracing'), true); }); + + test('addSentry adds package to sdk', () { + final file = File('test_resources/testfile.txt'); + + fixture.getSut( + file, + tracesSampleRate: 1.0, + ); + + expect( + fixture.hub.options.sdk.packages + .where((it) => it.name == packageName && it.version == sdkVersion) + .length, + 1, + ); + }); }); } diff --git a/file/test/version_test.dart b/file/test/version_test.dart index 57d12e357a..8bba9116b9 100644 --- a/file/test/version_test.dart +++ b/file/test/version_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: depend_on_referenced_packages - @TestOn('vm') import 'dart:io'; diff --git a/flutter/example/.gitignore b/flutter/example/.gitignore index 955a348605..c063aec688 100644 --- a/flutter/example/.gitignore +++ b/flutter/example/.gitignore @@ -39,3 +39,7 @@ app.*.map.json # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +# sqflite +web/sqflite_sw.js +web/sqlite3.wasm diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 5b6492a74e..e64975e44a 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -8,6 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; +import 'package:sqflite/sqflite.dart'; +// import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +// import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:feedback/feedback.dart' as feedback; import 'package:provider/provider.dart'; @@ -133,6 +137,10 @@ class MainScaffold extends StatelessWidget { child: Column( children: [ const Center(child: Text('Trigger an action:\n')), + ElevatedButton( + onPressed: () => sqfliteTest(), + child: const Text('sqflite'), + ), ElevatedButton( onPressed: () => SecondaryScaffold.openSecondaryScaffold(context), child: const Text('Open another Scaffold'), @@ -394,6 +402,52 @@ class MainScaffold extends StatelessWidget { ), ); } + + Future sqfliteTest() async { + final tr = Sentry.startTransaction( + 'sqfliteTest', + 'db', + bindToScope: true, + ); + + // databaseFactory = databaseFactoryFfiWeb; // or databaseFactoryFfi // or SentrySqfliteDatabaseFactory() + + // final sqfDb = await openDatabase(inMemoryDatabasePath); + final db = await openDatabaseWithSentry(inMemoryDatabasePath); + // final db = SentryDatabase(sqfDb); + // final batch = db.batch(); + await db.execute(''' + CREATE TABLE Product ( + id INTEGER PRIMARY KEY, + title TEXT + ) + '''); + final dbTitles = []; + for (int i = 1; i <= 20; i++) { + final title = 'Product $i'; + dbTitles.add(title); + await db.insert('Product', {'title': title}); + } + + await db.query('Product'); + + await db.transaction((txn) async { + await txn + .insert('Product', {'title': 'Product Another one'}); + await txn.delete('Product', + where: 'title = ?', whereArgs: ['Product Another one']); + }); + + await db.delete('Product', where: 'title = ?', whereArgs: ['Product 1']); + + // final batch = db.batch(); + // batch.delete('Product', where: 'title = ?', whereArgs: dbTitles); + // await batch.commit(); + + await db.close(); + + await tr.finish(status: const SpanStatus.ok()); + } } extension BuildContextExtension on BuildContext { diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 20172e1674..b9ae2cb0df 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -15,13 +15,17 @@ dependencies: sentry_flutter: sentry_dio: sentry_logging: + sentry_sqflite: universal_platform: ^1.0.0 feedback: ^2.0.0 provider: ^6.0.0 dio: any # This gets constrained by `sentry_dio` + sqflite: any # This gets constrained by `sentry_sqflite` logging: any # This gets constrained by `sentry_logging` package_info_plus: ^3.0.0 path_provider: ^2.0.0 + #sqflite_common_ffi: ^2.0.0 + #sqflite_common_ffi_web: ^0.3.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/flutter/example/pubspec_overrides.yaml b/flutter/example/pubspec_overrides.yaml index 832d30ee7e..50b018cd98 100644 --- a/flutter/example/pubspec_overrides.yaml +++ b/flutter/example/pubspec_overrides.yaml @@ -7,3 +7,5 @@ dependency_overrides: path: ../../dio sentry_logging: path: ../../logging + sentry_sqflite: + path: ../../sqflite diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 1dd4ab1839..0fbe6848ea 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -155,8 +155,7 @@ void main() { expect('test', image.debugFile); }); - test('Native layer is not called as there is no exceptions', - () async { + test('Native layer is not called as there is no exceptions', () async { var called = false; final sut = fixture.getSut(); diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index b5770d6dda..56f6f250a6 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -344,7 +344,7 @@ void main() { await Sentry.close(); }); - test('Web && (iOS || macOS) ', () async { + test('Web && (iOS || macOS)', () async { List integrations = []; Transport transport = MockTransport(); diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index e8c23baa7b..30ddf659dd 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -10,7 +10,7 @@ NEW_VERSION="${2}" echo "Current version: ${OLD_VERSION}" echo "Bumping version: ${NEW_VERSION}" -for pkg in {dart,flutter,logging,dio,file}; do +for pkg in {dart,flutter,logging,dio,file,sqflite}; do # Bump version in pubspec.yaml perl -pi -e "s/^version: .*/version: $NEW_VERSION/" $pkg/pubspec.yaml # Bump sentry dependency version in pubspec.yaml diff --git a/sqflite/.gitignore b/sqflite/.gitignore new file mode 100644 index 0000000000..ba521d5a39 --- /dev/null +++ b/sqflite/.gitignore @@ -0,0 +1,14 @@ +# Omit committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ diff --git a/sqflite/CHANGELOG.md b/sqflite/CHANGELOG.md new file mode 120000 index 0000000000..04c99a55ca --- /dev/null +++ b/sqflite/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/sqflite/LICENSE b/sqflite/LICENSE new file mode 100644 index 0000000000..2a6964d84d --- /dev/null +++ b/sqflite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sqflite/README.md b/sqflite/README.md new file mode 100644 index 0000000000..ed2b3c4f9d --- /dev/null +++ b/sqflite/README.md @@ -0,0 +1,69 @@ +

+ + + +
+

+ +Sentry integration for `sqflite` package +=========== + +| package | build | pub | likes | popularity | pub points | +| ------- | ------- | ------- | ------- | ------- | ------- | +| sentry_sqflite | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-sqflite/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-sqflite) | [![pub package](https://img.shields.io/pub/v/sentry_sqflite.svg)](https://pub.dev/packages/sentry_sqflite) | [![likes](https://img.shields.io/pub/likes/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) | [![pub points](https://img.shields.io/pub/points/sentry_sqflite)](https://pub.dev/packages/sentry_sqflite/score) + +Integration for the [`sqflite`](https://pub.dev/packages/sqflite) package. + +#### Usage + +- Sign up for a Sentry.io account and get a DSN at https://sentry.io. + +- Follow the installing instructions on [pub.dev](https://pub.dev/packages/sentry/install). + +- Initialize the Sentry SDK using the DSN issued by Sentry.io. + +- Call... + +```dart +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; + +Future main() async { + await SentryFlutter.init( + (options) { + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; + options.tracesSampleRate = 1.0; + }, + // Init your App. + appRunner: () => runApp(MyApp()), + ); +} + +Future insertProducts() async { + databaseFactory = SentrySqfliteDatabaseFactory(); + + final db = await openDatabase(inMemoryDatabasePath); + await db.execute(''' + CREATE TABLE Product ( + id INTEGER PRIMARY KEY, + title TEXT + ) + '''); + await db.insert('Product', {'title': 'Product 1'}); + await db.insert('Product', {'title': 'Product 2'}); + + final result = await db.query('Product'); + print(result); + + await db.close(); +} +``` + +#### Resources + +* [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dart/) +* [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) +* [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) +* [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) +* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) diff --git a/sqflite/analysis_options.yaml b/sqflite/analysis_options.yaml new file mode 100644 index 0000000000..92c8931384 --- /dev/null +++ b/sqflite/analysis_options.yaml @@ -0,0 +1,33 @@ +include: package:lints/recommended.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: error + # treat missing returns as a warning (not a hint) + missing_return: error + # allow having TODOs in the code + todo: ignore + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: warning + # ignore sentry/path on pubspec as we change it on deployment + invalid_dependency: ignore + unnecessary_import: ignore + exclude: + - example/** + +linter: + rules: + - prefer_final_locals + - public_member_api_docs + - prefer_single_quotes + - prefer_relative_imports + - unnecessary_brace_in_string_interps + - implementation_imports + - require_trailing_commas + - unawaited_futures diff --git a/sqflite/example/example.dart b/sqflite/example/example.dart new file mode 100644 index 0000000000..95ca49f434 --- /dev/null +++ b/sqflite/example/example.dart @@ -0,0 +1,37 @@ +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; + +Future main() async { + // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io + const dsn = + 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; + + await Sentry.init( + (options) { + options.dsn = dsn; + options.tracesSampleRate = 1.0; + options.debug = true; + }, + appRunner: runApp, // Init your App. + ); +} + +Future runApp() async { + databaseFactory = SentrySqfliteDatabaseFactory(); + + final db = await openDatabase(inMemoryDatabasePath); + await db.execute(''' + CREATE TABLE Product ( + id INTEGER PRIMARY KEY, + title TEXT + ) + '''); + await db.insert('Product', {'title': 'Product 1'}); + await db.insert('Product', {'title': 'Product 2'}); + + final result = await db.query('Product'); + print(result); + + await db.close(); +} diff --git a/sqflite/lib/sentry_sqflite.dart b/sqflite/lib/sentry_sqflite.dart new file mode 100644 index 0000000000..770831fb5b --- /dev/null +++ b/sqflite/lib/sentry_sqflite.dart @@ -0,0 +1,7 @@ +library sentry_sqflite; + +export 'src/sentry_database.dart'; +export 'src/sentry_sqflite.dart'; +export 'src/sentry_sqflite_database_factory.dart'; +export 'src/sentry_batch.dart'; +export 'src/sentry_sqflite_transaction.dart'; diff --git a/sqflite/lib/src/sentry_batch.dart b/sqflite/lib/src/sentry_batch.dart new file mode 100644 index 0000000000..79cabcdb08 --- /dev/null +++ b/sqflite/lib/src/sentry_batch.dart @@ -0,0 +1,246 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; + +// ignore: implementation_imports +import 'package:sqflite_common/src/sql_builder.dart'; + +import 'sentry_database.dart'; + +/// A [Batch] wrapper that adds Sentry support. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// import 'package:sentry_sqflite/sentry_sqflite.dart'; +/// +/// final database = await openDatabase('path/to/db'); +/// final sentryDatabase = SentryDatabase(database); +/// final batch = sentryDatabase.batch(); +/// ``` +@experimental +class SentryBatch implements Batch { + final Batch _batch; + final Hub _hub; + + // we don't clear the buffer because SqfliteBatch don't either + final _buffer = StringBuffer(); + + /// ```dart + /// import 'package:sqflite/sqflite.dart'; + /// import 'package:sentry_sqflite/sentry_sqflite.dart'; + /// + /// final database = await openDatabase('path/to/db'); + /// final sentryDatabase = SentryDatabase(database); + /// final batch = sentryDatabase.batch(); + /// ``` + SentryBatch( + this._batch, { + @internal Hub? hub, + }) : _hub = hub ?? HubAdapter(); + + @override + Future> apply({bool? noResult, bool? continueOnError}) { + Future> future() async { + final currentSpan = _hub.getSpan(); + + final span = currentSpan?.startChild( + SentryDatabase.dbOp, + description: _buffer.toString().trim(), + ); + + try { + final result = await _batch.apply( + noResult: noResult, + continueOnError: continueOnError, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future> commit({ + bool? exclusive, + bool? noResult, + bool? continueOnError, + }) { + Future> future() async { + final currentSpan = _hub.getSpan(); + + final span = currentSpan?.startChild( + SentryDatabase.dbOp, + description: _buffer.toString().trim(), + ); + + try { + final result = await _batch.commit( + exclusive: exclusive, + noResult: noResult, + continueOnError: continueOnError, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + void delete(String table, {String? where, List? whereArgs}) { + final builder = + SqlBuilder.delete(table, where: where, whereArgs: whereArgs); + _buffer.writeln(builder.sql); + + _batch.delete(table, where: where, whereArgs: whereArgs); + } + + @override + void execute(String sql, [List? arguments]) { + _buffer.writeln(sql); + + _batch.execute(sql, arguments); + } + + @override + void insert( + String table, + Map values, { + String? nullColumnHack, + ConflictAlgorithm? conflictAlgorithm, + }) { + final builder = SqlBuilder.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); + _buffer.writeln(builder.sql); + + _batch.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); + } + + @override + int get length => _batch.length; + + @override + void query( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) { + final builder = SqlBuilder.query( + table, + distinct: distinct, + columns: columns, + where: where, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + whereArgs: whereArgs, + ); + _buffer.writeln(builder.sql); + + _batch.query( + table, + distinct: distinct, + columns: columns, + where: where, + whereArgs: whereArgs, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + ); + } + + @override + void rawDelete(String sql, [List? arguments]) { + _buffer.writeln(sql); + + _batch.rawDelete(sql, arguments); + } + + @override + void rawInsert(String sql, [List? arguments]) { + _buffer.writeln(sql); + + _batch.rawInsert(sql, arguments); + } + + @override + void rawQuery(String sql, [List? arguments]) { + _buffer.writeln(sql); + + _batch.rawQuery(sql, arguments); + } + + @override + void rawUpdate(String sql, [List? arguments]) { + _buffer.writeln(sql); + + _batch.rawUpdate(sql, arguments); + } + + @override + void update( + String table, + Map values, { + String? where, + List? whereArgs, + ConflictAlgorithm? conflictAlgorithm, + }) { + final builder = SqlBuilder.update( + table, + values, + where: where, + whereArgs: whereArgs, + conflictAlgorithm: conflictAlgorithm, + ); + _buffer.writeln(builder.sql); + + _batch.update( + table, + where: where, + values, + whereArgs: whereArgs, + conflictAlgorithm: conflictAlgorithm, + ); + } +} diff --git a/sqflite/lib/src/sentry_database.dart b/sqflite/lib/src/sentry_database.dart new file mode 100644 index 0000000000..3af501caf0 --- /dev/null +++ b/sqflite/lib/src/sentry_database.dart @@ -0,0 +1,143 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; + +import 'sentry_database_executor.dart'; +import 'sentry_sqflite_transaction.dart'; +import 'version.dart'; + +/// A [Database] wrapper that adds Sentry support. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// import 'package:sentry_sqflite/sentry_sqflite.dart'; +/// +/// final database = await openDatabase('path/to/db'); +/// final sentryDatabase = SentryDatabase(database); +/// ``` +@experimental +class SentryDatabase extends SentryDatabaseExecutor implements Database { + final Database _database; + final Hub _hub; + + @internal + // ignore: public_member_api_docs + static const dbOp = 'db'; + @internal + // ignore: public_member_api_docs + static const dbSqlExecuteOp = 'db.sql.execute'; + @internal + // ignore: public_member_api_docs + static const dbSqlQueryOp = 'db.sql.query'; + + static const _dbSqlOp = 'db.sql.transaction'; + + /// ```dart + /// import 'package:sqflite/sqflite.dart'; + /// import 'package:sentry_sqflite/sentry_sqflite.dart'; + /// + /// final database = await openDatabase('path/to/db'); + /// final sentryDatabase = SentryDatabase(database); + /// ``` + SentryDatabase( + this._database, { + @internal Hub? hub, + }) : _hub = hub ?? HubAdapter(), + super(_database, hub: hub) { + // ignore: invalid_use_of_internal_member + final options = _hub.options; + options.sdk.addIntegration('SentrySqfliteTracing'); + options.sdk.addPackage(packageName, sdkVersion); + } + + // TODO: check if perf is enabled + + @override + Future close() { + Future future() async { + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild( + dbOp, + description: 'Close DB: ${_database.path}', + ); + + try { + await _database.close(); + + span?.status = SpanStatus.ok(); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future devInvokeMethod(String method, [Object? arguments]) { + // ignore: deprecated_member_use + return _database.devInvokeMethod(method, arguments); + } + + @override + Future devInvokeSqlMethod( + String method, + String sql, [ + List? arguments, + ]) { + // ignore: deprecated_member_use + return _database.devInvokeSqlMethod(method, sql); + } + + @override + bool get isOpen => _database.isOpen; + + @override + String get path => _database.path; + + @override + Future transaction( + Future Function(Transaction txn) action, { + bool? exclusive, + }) { + Future future() async { + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild( + _dbSqlOp, + description: 'Transaction DB: ${_database.path}', + ); + + Future newAction(Transaction txn) async { + final executor = + SentryDatabaseExecutor(txn, parentSpan: span, hub: _hub); + final sentrySqfliteTransaction = + SentrySqfliteTransaction(executor, hub: _hub); + + return await action(sentrySqfliteTransaction); + } + + try { + final result = + await _database.transaction(newAction, exclusive: exclusive); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } +} diff --git a/sqflite/lib/src/sentry_database_executor.dart b/sqflite/lib/src/sentry_database_executor.dart new file mode 100644 index 0000000000..4284de79f0 --- /dev/null +++ b/sqflite/lib/src/sentry_database_executor.dart @@ -0,0 +1,456 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; + +// ignore: implementation_imports +import 'package:sqflite_common/src/sql_builder.dart'; + +import 'sentry_batch.dart'; +import 'sentry_database.dart'; + +@internal +// ignore: public_member_api_docs +class SentryDatabaseExecutor implements DatabaseExecutor { + final DatabaseExecutor _executor; + final ISentrySpan? _parentSpan; + + // ignore: public_member_api_docs + SentryDatabaseExecutor( + this._executor, { + ISentrySpan? parentSpan, + @internal Hub? hub, + }) : _parentSpan = parentSpan, + _hub = hub ?? HubAdapter(); + final Hub _hub; + + @override + Batch batch() => SentryBatch(_executor.batch(), hub: _hub); + + @override + Database get database => _executor.database; + + @override + Future delete(String table, {String? where, List? whereArgs}) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final builder = + SqlBuilder.delete(table, where: where, whereArgs: whereArgs); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: builder.sql, + ); + + try { + final result = + await _executor.delete(table, where: where, whereArgs: whereArgs); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future execute(String sql, [List? arguments]) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: sql, + ); + + try { + await _executor.execute(sql, arguments); + + span?.status = SpanStatus.ok(); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future insert( + String table, + Map values, { + String? nullColumnHack, + ConflictAlgorithm? conflictAlgorithm, + }) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final builder = SqlBuilder.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: builder.sql, + ); + + try { + final result = await _executor.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future>> query( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) { + Future>> future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final builder = SqlBuilder.query( + table, + distinct: distinct, + columns: columns, + where: where, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + whereArgs: whereArgs, + ); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlQueryOp, + description: builder.sql, + ); + + try { + final result = await _executor.query( + table, + distinct: distinct, + columns: columns, + where: where, + whereArgs: whereArgs, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future queryCursor( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + int? bufferSize, + }) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final builder = SqlBuilder.query( + table, + distinct: distinct, + columns: columns, + where: where, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + whereArgs: whereArgs, + ); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlQueryOp, + description: builder.sql, + ); + + try { + final result = await _executor.queryCursor( + table, + distinct: distinct, + columns: columns, + where: where, + whereArgs: whereArgs, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + bufferSize: bufferSize, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future rawDelete(String sql, [List? arguments]) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: sql, + ); + + try { + final result = await _executor.rawDelete(sql, arguments); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future rawInsert(String sql, [List? arguments]) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: sql, + ); + + try { + final result = await _executor.rawInsert(sql, arguments); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future>> rawQuery( + String sql, [ + List? arguments, + ]) { + Future>> future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlQueryOp, + description: sql, + ); + + try { + final result = await _executor.rawQuery(sql, arguments); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future rawQueryCursor( + String sql, + List? arguments, { + int? bufferSize, + }) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlQueryOp, + description: sql, + ); + + try { + final result = await _executor.rawQueryCursor( + sql, + arguments, + bufferSize: bufferSize, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future rawUpdate(String sql, [List? arguments]) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: sql, + ); + + try { + final result = await _executor.rawUpdate(sql, arguments); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } + + @override + Future update( + String table, + Map values, { + String? where, + List? whereArgs, + ConflictAlgorithm? conflictAlgorithm, + }) { + Future future() async { + final currentSpan = _parentSpan ?? _hub.getSpan(); + final builder = SqlBuilder.update( + table, + values, + where: where, + whereArgs: whereArgs, + conflictAlgorithm: conflictAlgorithm, + ); + final span = currentSpan?.startChild( + SentryDatabase.dbSqlExecuteOp, + description: builder.sql, + ); + + try { + final result = await _executor.update( + table, + values, + where: where, + whereArgs: whereArgs, + conflictAlgorithm: conflictAlgorithm, + ); + + span?.status = SpanStatus.ok(); + + return result; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return future(); + } +} diff --git a/sqflite/lib/src/sentry_sqflite.dart b/sqflite/lib/src/sentry_sqflite.dart new file mode 100644 index 0000000000..ef8542186b --- /dev/null +++ b/sqflite/lib/src/sentry_sqflite.dart @@ -0,0 +1,83 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; + +import 'sentry_database.dart'; + +/// Opens a database with Sentry support. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// import 'package:sentry_sqflite/sentry_sqflite.dart'; +/// +/// final database = await openDatabaseWithSentry('path/to/db'); +/// ``` +@experimental +Future openDatabaseWithSentry( + String path, { + int? version, + OnDatabaseConfigureFn? onConfigure, + OnDatabaseCreateFn? onCreate, + OnDatabaseVersionChangeFn? onUpgrade, + OnDatabaseVersionChangeFn? onDowngrade, + OnDatabaseOpenFn? onOpen, + bool readOnly = false, + bool singleInstance = true, + @internal Hub? hub, +}) { + Future openDatabase() async { + final dbOptions = OpenDatabaseOptions( + version: version, + onConfigure: onConfigure, + onCreate: onCreate, + onUpgrade: onUpgrade, + onDowngrade: onDowngrade, + onOpen: onOpen, + readOnly: readOnly, + singleInstance: singleInstance, + ); + + final newHub = hub ?? HubAdapter(); + + final currentSpan = newHub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbOp, + description: 'Open DB: $path', + ); + + try { + final database = + await databaseFactory.openDatabase(path, options: dbOptions); + + final sentryDatabase = SentryDatabase(database, hub: newHub); + + span?.status = SpanStatus.ok(); + return sentryDatabase; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return openDatabase(); +} + +/// Opens a database with Sentry support. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// import 'package:sentry_sqflite/sentry_sqflite.dart'; +/// +/// final database = await openReadOnlyDatabaseWithSentry('path/to/db'); +/// ``` +@experimental +Future openReadOnlyDatabaseWithSentry( + String path, { + @internal Hub? hub, +}) { + return openDatabaseWithSentry(path, readOnly: true, hub: hub); +} diff --git a/sqflite/lib/src/sentry_sqflite_database_factory.dart b/sqflite/lib/src/sentry_sqflite_database_factory.dart new file mode 100644 index 0000000000..c4c7dcd32e --- /dev/null +++ b/sqflite/lib/src/sentry_sqflite_database_factory.dart @@ -0,0 +1,87 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; +// ignore: implementation_imports +import 'package:sqflite_common/src/factory_mixin.dart'; +// ignore: implementation_imports +import 'package:sqflite/src/sqflite_impl.dart' as impl; + +import 'sentry_database.dart'; + +/// Using this factory, all [Database] instances will be wrapped with Sentry. +/// +/// Only use the factory if you want to wrap all [Database] instances even from +/// 3rd party libraries and SDKs, otherwise prefer the [openDatabaseWithSentry] +/// or [SentryDatabase] constructor. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// +/// databaseFactory = SentrySqfliteDatabaseFactory(); +/// // or SentrySqfliteDatabaseFactory(databaseFactory: databaseFactoryFfi); +/// // if you are using the FFI or Web implementation. +/// +/// final database = await openDatabase('path/to/db'); +/// ``` +@experimental +class SentrySqfliteDatabaseFactory with SqfliteDatabaseFactoryMixin { + /// ```dart + /// import 'package:sqflite/sqflite.dart'; + /// + /// databaseFactory = SentrySqfliteDatabaseFactory(); + /// + /// final database = await openDatabase('path/to/db'); + /// ``` + SentrySqfliteDatabaseFactory({ + DatabaseFactory? databaseFactory, + @internal Hub? hub, + }) : _databaseFactory = databaseFactory, + _hub = hub ?? HubAdapter(); + + final Hub _hub; + final DatabaseFactory? _databaseFactory; + + @override + Future invokeMethod(String method, [Object? arguments]) => + impl.invokeMethod(method, arguments); + + @override + Future openDatabase( + String path, { + OpenDatabaseOptions? options, + }) async { + final databaseFactory = _databaseFactory ?? this; + + // ignore: invalid_use_of_internal_member + if (!_hub.options.isTracingEnabled()) { + return databaseFactory.openDatabase(path, options: options); + } + + Future openDatabase() async { + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild( + SentryDatabase.dbOp, + description: 'Open DB: $path', + ); + + try { + final database = + await databaseFactory.openDatabase(path, options: options); + + final sentryDatabase = SentryDatabase(database, hub: _hub); + + span?.status = SpanStatus.ok(); + return sentryDatabase; + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + + rethrow; + } finally { + await span?.finish(); + } + } + + return openDatabase(); + } +} diff --git a/sqflite/lib/src/sentry_sqflite_transaction.dart b/sqflite/lib/src/sentry_sqflite_transaction.dart new file mode 100644 index 0000000000..c207023e9c --- /dev/null +++ b/sqflite/lib/src/sentry_sqflite_transaction.dart @@ -0,0 +1,160 @@ +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sqflite/sqflite.dart'; + +import 'sentry_batch.dart'; + +/// A [Transaction] wrapper that adds Sentry support. +/// +/// ```dart +/// import 'package:sqflite/sqflite.dart'; +/// import 'package:sentry_sqflite/sentry_sqflite.dart'; +/// +/// final database = await openDatabase('path/to/db'); +/// final sentryDatabase = SentryDatabase(database); +/// +/// await sentryDatabase.transaction((txn) async { +/// ... +/// }); +/// ``` +@experimental +class SentrySqfliteTransaction extends Transaction implements DatabaseExecutor { + final DatabaseExecutor _executor; + final Hub _hub; + + // ignore: public_member_api_docs + @internal + SentrySqfliteTransaction( + this._executor, { + @internal Hub? hub, + }) : _hub = hub ?? HubAdapter(); + + @override + Batch batch() => SentryBatch(_executor.batch(), hub: _hub); + + @override + Database get database => _executor.database; + + @override + Future delete(String table, {String? where, List? whereArgs}) => + _executor.delete( + table, + where: where, + whereArgs: whereArgs, + ); + + @override + Future execute(String sql, [List? arguments]) => + _executor.execute(sql, arguments); + + @override + Future insert( + String table, + Map values, { + String? nullColumnHack, + ConflictAlgorithm? conflictAlgorithm, + }) => + _executor.insert( + table, + values, + nullColumnHack: nullColumnHack, + conflictAlgorithm: conflictAlgorithm, + ); + + @override + Future>> query( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) => + _executor.query( + table, + distinct: distinct, + columns: columns, + where: where, + whereArgs: whereArgs, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + ); + + @override + Future queryCursor( + String table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + int? bufferSize, + }) => + _executor.queryCursor( + table, + distinct: distinct, + columns: columns, + where: where, + whereArgs: whereArgs, + groupBy: groupBy, + having: having, + orderBy: orderBy, + limit: limit, + offset: offset, + bufferSize: bufferSize, + ); + + @override + Future rawDelete(String sql, [List? arguments]) => + _executor.rawDelete(sql, arguments); + + @override + Future rawInsert(String sql, [List? arguments]) => + _executor.rawInsert(sql, arguments); + + @override + Future>> rawQuery( + String sql, [ + List? arguments, + ]) => + _executor.rawQuery(sql, arguments); + + @override + Future rawQueryCursor( + String sql, + List? arguments, { + int? bufferSize, + }) => + _executor.rawQueryCursor(sql, arguments, bufferSize: bufferSize); + + @override + Future rawUpdate(String sql, [List? arguments]) => + _executor.rawUpdate(sql, arguments); + + @override + Future update( + String table, + Map values, { + String? where, + List? whereArgs, + ConflictAlgorithm? conflictAlgorithm, + }) => + _executor.update( + table, + values, + where: where, + whereArgs: whereArgs, + conflictAlgorithm: conflictAlgorithm, + ); +} diff --git a/sqflite/lib/src/version.dart b/sqflite/lib/src/version.dart new file mode 100644 index 0000000000..79d8ab5009 --- /dev/null +++ b/sqflite/lib/src/version.dart @@ -0,0 +1,5 @@ +/// The SDK version reported to Sentry.io in the submitted events. +const String sdkVersion = '7.1.0'; + +/// The package name reported to Sentry.io in the submitted events. +const String packageName = 'pub:sentry_sqflite'; diff --git a/sqflite/pubspec.yaml b/sqflite/pubspec.yaml new file mode 100644 index 0000000000..82af304c28 --- /dev/null +++ b/sqflite/pubspec.yaml @@ -0,0 +1,26 @@ +name: sentry_sqflite +description: An integration which adds support for performance tracing for the sqflite package. +version: 7.1.0 +homepage: https://docs.sentry.io/platforms/flutter/ +repository: https://github.com/getsentry/sentry-dart +issue_tracker: https://github.com/getsentry/sentry-dart/issues + +environment: + sdk: '>=2.17.0 <4.0.0' + flutter: '>=3.3.0' # matching sqflite + +dependencies: + sentry: 7.1.0 + sqflite: ^2.0.0 + sqflite_common: ^2.0.0 + meta: ^1.3.0 + +dev_dependencies: + lints: ^2.0.0 + flutter_test: + sdk: flutter + coverage: ^1.3.0 + mockito: ^5.1.0 + build_runner: ^2.1.8 + yaml: ^3.1.0 # needed for version match (code and pubspec) + sqflite_common_ffi: ^2.0.0 diff --git a/sqflite/pubspec_overrides.yaml b/sqflite/pubspec_overrides.yaml new file mode 100644 index 0000000000..16e71d16f0 --- /dev/null +++ b/sqflite/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + sentry: + path: ../dart diff --git a/sqflite/test/mocks/mocks.dart b/sqflite/test/mocks/mocks.dart new file mode 100644 index 0000000000..0a3115df74 --- /dev/null +++ b/sqflite/test/mocks/mocks.dart @@ -0,0 +1,39 @@ +import 'package:mockito/annotations.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sqflite/sqflite.dart'; + +import 'mocks.mocks.dart'; + +ISentrySpan startTransactionShim( + String? name, + String? operation, { + String? description, + DateTime? startTimestamp, + bool? bindToScope, + bool? waitForChildren, + Duration? autoFinishAfter, + bool? trimEnd, + OnTransactionFinish? onFinish, + Map? customSamplingContext, +}) { + return MockSentryTracer(); +} + +// From a directory that contains a pubspec.yaml file: +// dart run build_runner build # Dart SDK +// flutter pub run build_runner build # Flutter SDK + +@GenerateMocks( + [ + // ignore: invalid_use_of_internal_member + SentryTracer, + Batch, + Database, + DatabaseExecutor, + ], + customMocks: [ + MockSpec(fallbackGenerators: {#startTransaction: startTransactionShim}) + ], +) +void main() {} diff --git a/sqflite/test/mocks/mocks.mocks.dart b/sqflite/test/mocks/mocks.mocks.dart new file mode 100644 index 0000000000..5af57a4e2b --- /dev/null +++ b/sqflite/test/mocks/mocks.mocks.dart @@ -0,0 +1,1538 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in sentry_sqflite/test/mocks/mocks.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:sentry/sentry.dart' as _i2; +import 'package:sentry/src/sentry_tracer.dart' as _i4; +import 'package:sqflite_common/sql.dart' as _i6; +import 'package:sqflite_common/sqlite_api.dart' as _i3; + +import 'mocks.dart' as _i7; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeSentrySpanContext_0 extends _i1.SmartFake + implements _i2.SentrySpanContext { + _FakeSentrySpanContext_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDateTime_1 extends _i1.SmartFake implements DateTime { + _FakeDateTime_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeISentrySpan_2 extends _i1.SmartFake implements _i2.ISentrySpan { + _FakeISentrySpan_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSentryTraceHeader_3 extends _i1.SmartFake + implements _i2.SentryTraceHeader { + _FakeSentryTraceHeader_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDatabase_4 extends _i1.SmartFake implements _i3.Database { + _FakeDatabase_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeQueryCursor_5 extends _i1.SmartFake implements _i3.QueryCursor { + _FakeQueryCursor_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBatch_6 extends _i1.SmartFake implements _i3.Batch { + _FakeBatch_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSentryOptions_7 extends _i1.SmartFake implements _i2.SentryOptions { + _FakeSentryOptions_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSentryId_8 extends _i1.SmartFake implements _i2.SentryId { + _FakeSentryId_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHub_9 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [SentryTracer]. +/// +/// See the documentation for Mockito's code generation for more information. +/// ignore: invalid_use_of_internal_member +class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { + MockSentryTracer() { + _i1.throwOnMissingStub(this); + } + + @override + String get name => (super.noSuchMethod( + Invocation.getter(#name), + returnValue: '', + ) as String); + @override + set name(String? _name) => super.noSuchMethod( + Invocation.setter( + #name, + _name, + ), + returnValueForMissingStub: null, + ); + @override + _i2.SentryTransactionNameSource get transactionNameSource => + (super.noSuchMethod( + Invocation.getter(#transactionNameSource), + returnValue: _i2.SentryTransactionNameSource.custom, + ) as _i2.SentryTransactionNameSource); + @override + set transactionNameSource( + _i2.SentryTransactionNameSource? _transactionNameSource) => + super.noSuchMethod( + Invocation.setter( + #transactionNameSource, + _transactionNameSource, + ), + returnValueForMissingStub: null, + ); + @override + _i2.SentrySpanContext get context => (super.noSuchMethod( + Invocation.getter(#context), + returnValue: _FakeSentrySpanContext_0( + this, + Invocation.getter(#context), + ), + ) as _i2.SentrySpanContext); + @override + DateTime get startTimestamp => (super.noSuchMethod( + Invocation.getter(#startTimestamp), + returnValue: _FakeDateTime_1( + this, + Invocation.getter(#startTimestamp), + ), + ) as DateTime); + @override + Map get data => (super.noSuchMethod( + Invocation.getter(#data), + returnValue: {}, + ) as Map); + @override + bool get finished => (super.noSuchMethod( + Invocation.getter(#finished), + returnValue: false, + ) as bool); + @override + List<_i2.SentrySpan> get children => (super.noSuchMethod( + Invocation.getter(#children), + returnValue: <_i2.SentrySpan>[], + ) as List<_i2.SentrySpan>); + @override + set throwable(dynamic throwable) => super.noSuchMethod( + Invocation.setter( + #throwable, + throwable, + ), + returnValueForMissingStub: null, + ); + @override + set status(_i2.SpanStatus? status) => super.noSuchMethod( + Invocation.setter( + #status, + status, + ), + returnValueForMissingStub: null, + ); + @override + Map get tags => (super.noSuchMethod( + Invocation.getter(#tags), + returnValue: {}, + ) as Map); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override + _i5.Future finish({ + _i2.SpanStatus? status, + DateTime? endTimestamp, + }) => + (super.noSuchMethod( + Invocation.method( + #finish, + [], + { + #status: status, + #endTimestamp: endTimestamp, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + void removeData(String? key) => super.noSuchMethod( + Invocation.method( + #removeData, + [key], + ), + returnValueForMissingStub: null, + ); + @override + void removeTag(String? key) => super.noSuchMethod( + Invocation.method( + #removeTag, + [key], + ), + returnValueForMissingStub: null, + ); + @override + void setData( + String? key, + dynamic value, + ) => + super.noSuchMethod( + Invocation.method( + #setData, + [ + key, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setTag( + String? key, + String? value, + ) => + super.noSuchMethod( + Invocation.method( + #setTag, + [ + key, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i2.ISentrySpan startChild( + String? operation, { + String? description, + DateTime? startTimestamp, + }) => + (super.noSuchMethod( + Invocation.method( + #startChild, + [operation], + { + #description: description, + #startTimestamp: startTimestamp, + }, + ), + returnValue: _FakeISentrySpan_2( + this, + Invocation.method( + #startChild, + [operation], + { + #description: description, + #startTimestamp: startTimestamp, + }, + ), + ), + ) as _i2.ISentrySpan); + @override + _i2.ISentrySpan startChildWithParentSpanId( + _i2.SpanId? parentSpanId, + String? operation, { + String? description, + DateTime? startTimestamp, + }) => + (super.noSuchMethod( + Invocation.method( + #startChildWithParentSpanId, + [ + parentSpanId, + operation, + ], + { + #description: description, + #startTimestamp: startTimestamp, + }, + ), + returnValue: _FakeISentrySpan_2( + this, + Invocation.method( + #startChildWithParentSpanId, + [ + parentSpanId, + operation, + ], + { + #description: description, + #startTimestamp: startTimestamp, + }, + ), + ), + ) as _i2.ISentrySpan); + @override + _i2.SentryTraceHeader toSentryTrace() => (super.noSuchMethod( + Invocation.method( + #toSentryTrace, + [], + ), + returnValue: _FakeSentryTraceHeader_3( + this, + Invocation.method( + #toSentryTrace, + [], + ), + ), + ) as _i2.SentryTraceHeader); + @override + void setMeasurement( + String? name, + num? value, { + _i2.SentryMeasurementUnit? unit, + }) => + super.noSuchMethod( + Invocation.method( + #setMeasurement, + [ + name, + value, + ], + {#unit: unit}, + ), + returnValueForMissingStub: null, + ); + @override + void scheduleFinish() => super.noSuchMethod( + Invocation.method( + #scheduleFinish, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Batch]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBatch extends _i1.Mock implements _i3.Batch { + MockBatch() { + _i1.throwOnMissingStub(this); + } + + @override + int get length => (super.noSuchMethod( + Invocation.getter(#length), + returnValue: 0, + ) as int); + @override + _i5.Future> commit({ + bool? exclusive, + bool? noResult, + bool? continueOnError, + }) => + (super.noSuchMethod( + Invocation.method( + #commit, + [], + { + #exclusive: exclusive, + #noResult: noResult, + #continueOnError: continueOnError, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + @override + _i5.Future> apply({ + bool? noResult, + bool? continueOnError, + }) => + (super.noSuchMethod( + Invocation.method( + #apply, + [], + { + #noResult: noResult, + #continueOnError: continueOnError, + }, + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + @override + void rawInsert( + String? sql, [ + List? arguments, + ]) => + super.noSuchMethod( + Invocation.method( + #rawInsert, + [ + sql, + arguments, + ], + ), + returnValueForMissingStub: null, + ); + @override + void insert( + String? table, + Map? values, { + String? nullColumnHack, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + super.noSuchMethod( + Invocation.method( + #insert, + [ + table, + values, + ], + { + #nullColumnHack: nullColumnHack, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValueForMissingStub: null, + ); + @override + void rawUpdate( + String? sql, [ + List? arguments, + ]) => + super.noSuchMethod( + Invocation.method( + #rawUpdate, + [ + sql, + arguments, + ], + ), + returnValueForMissingStub: null, + ); + @override + void update( + String? table, + Map? values, { + String? where, + List? whereArgs, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + super.noSuchMethod( + Invocation.method( + #update, + [ + table, + values, + ], + { + #where: where, + #whereArgs: whereArgs, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValueForMissingStub: null, + ); + @override + void rawDelete( + String? sql, [ + List? arguments, + ]) => + super.noSuchMethod( + Invocation.method( + #rawDelete, + [ + sql, + arguments, + ], + ), + returnValueForMissingStub: null, + ); + @override + void delete( + String? table, { + String? where, + List? whereArgs, + }) => + super.noSuchMethod( + Invocation.method( + #delete, + [table], + { + #where: where, + #whereArgs: whereArgs, + }, + ), + returnValueForMissingStub: null, + ); + @override + void execute( + String? sql, [ + List? arguments, + ]) => + super.noSuchMethod( + Invocation.method( + #execute, + [ + sql, + arguments, + ], + ), + returnValueForMissingStub: null, + ); + @override + void query( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) => + super.noSuchMethod( + Invocation.method( + #query, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + }, + ), + returnValueForMissingStub: null, + ); + @override + void rawQuery( + String? sql, [ + List? arguments, + ]) => + super.noSuchMethod( + Invocation.method( + #rawQuery, + [ + sql, + arguments, + ], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Database]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDatabase extends _i1.Mock implements _i3.Database { + MockDatabase() { + _i1.throwOnMissingStub(this); + } + + @override + String get path => (super.noSuchMethod( + Invocation.getter(#path), + returnValue: '', + ) as String); + @override + bool get isOpen => (super.noSuchMethod( + Invocation.getter(#isOpen), + returnValue: false, + ) as bool); + @override + _i3.Database get database => (super.noSuchMethod( + Invocation.getter(#database), + returnValue: _FakeDatabase_4( + this, + Invocation.getter(#database), + ), + ) as _i3.Database); + @override + _i5.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future transaction( + _i5.Future Function(_i3.Transaction)? action, { + bool? exclusive, + }) => + (super.noSuchMethod( + Invocation.method( + #transaction, + [action], + {#exclusive: exclusive}, + ), + returnValue: _i5.Future.value(null), + ) as _i5.Future); + @override + _i5.Future devInvokeMethod( + String? method, [ + Object? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #devInvokeMethod, + [ + method, + arguments, + ], + ), + returnValue: _i5.Future.value(null), + ) as _i5.Future); + @override + _i5.Future devInvokeSqlMethod( + String? method, + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #devInvokeSqlMethod, + [ + method, + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(null), + ) as _i5.Future); + @override + _i5.Future execute( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #execute, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future rawInsert( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawInsert, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future insert( + String? table, + Map? values, { + String? nullColumnHack, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #insert, + [ + table, + values, + ], + { + #nullColumnHack: nullColumnHack, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future>> query( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) => + (super.noSuchMethod( + Invocation.method( + #query, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + }, + ), + returnValue: _i5.Future>>.value( + >[]), + ) as _i5.Future>>); + @override + _i5.Future>> rawQuery( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawQuery, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future>>.value( + >[]), + ) as _i5.Future>>); + @override + _i5.Future<_i3.QueryCursor> rawQueryCursor( + String? sql, + List? arguments, { + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + returnValue: _i5.Future<_i3.QueryCursor>.value(_FakeQueryCursor_5( + this, + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + )), + ) as _i5.Future<_i3.QueryCursor>); + @override + _i5.Future<_i3.QueryCursor> queryCursor( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + returnValue: _i5.Future<_i3.QueryCursor>.value(_FakeQueryCursor_5( + this, + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + )), + ) as _i5.Future<_i3.QueryCursor>); + @override + _i5.Future rawUpdate( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawUpdate, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future update( + String? table, + Map? values, { + String? where, + List? whereArgs, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #update, + [ + table, + values, + ], + { + #where: where, + #whereArgs: whereArgs, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future rawDelete( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawDelete, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future delete( + String? table, { + String? where, + List? whereArgs, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [table], + { + #where: where, + #whereArgs: whereArgs, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i3.Batch batch() => (super.noSuchMethod( + Invocation.method( + #batch, + [], + ), + returnValue: _FakeBatch_6( + this, + Invocation.method( + #batch, + [], + ), + ), + ) as _i3.Batch); +} + +/// A class which mocks [DatabaseExecutor]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDatabaseExecutor extends _i1.Mock implements _i3.DatabaseExecutor { + MockDatabaseExecutor() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Database get database => (super.noSuchMethod( + Invocation.getter(#database), + returnValue: _FakeDatabase_4( + this, + Invocation.getter(#database), + ), + ) as _i3.Database); + @override + _i5.Future execute( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #execute, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future rawInsert( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawInsert, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future insert( + String? table, + Map? values, { + String? nullColumnHack, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #insert, + [ + table, + values, + ], + { + #nullColumnHack: nullColumnHack, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future>> query( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + }) => + (super.noSuchMethod( + Invocation.method( + #query, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + }, + ), + returnValue: _i5.Future>>.value( + >[]), + ) as _i5.Future>>); + @override + _i5.Future>> rawQuery( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawQuery, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future>>.value( + >[]), + ) as _i5.Future>>); + @override + _i5.Future<_i3.QueryCursor> rawQueryCursor( + String? sql, + List? arguments, { + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + returnValue: _i5.Future<_i3.QueryCursor>.value(_FakeQueryCursor_5( + this, + Invocation.method( + #rawQueryCursor, + [ + sql, + arguments, + ], + {#bufferSize: bufferSize}, + ), + )), + ) as _i5.Future<_i3.QueryCursor>); + @override + _i5.Future<_i3.QueryCursor> queryCursor( + String? table, { + bool? distinct, + List? columns, + String? where, + List? whereArgs, + String? groupBy, + String? having, + String? orderBy, + int? limit, + int? offset, + int? bufferSize, + }) => + (super.noSuchMethod( + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + returnValue: _i5.Future<_i3.QueryCursor>.value(_FakeQueryCursor_5( + this, + Invocation.method( + #queryCursor, + [table], + { + #distinct: distinct, + #columns: columns, + #where: where, + #whereArgs: whereArgs, + #groupBy: groupBy, + #having: having, + #orderBy: orderBy, + #limit: limit, + #offset: offset, + #bufferSize: bufferSize, + }, + ), + )), + ) as _i5.Future<_i3.QueryCursor>); + @override + _i5.Future rawUpdate( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawUpdate, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future update( + String? table, + Map? values, { + String? where, + List? whereArgs, + _i6.ConflictAlgorithm? conflictAlgorithm, + }) => + (super.noSuchMethod( + Invocation.method( + #update, + [ + table, + values, + ], + { + #where: where, + #whereArgs: whereArgs, + #conflictAlgorithm: conflictAlgorithm, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future rawDelete( + String? sql, [ + List? arguments, + ]) => + (super.noSuchMethod( + Invocation.method( + #rawDelete, + [ + sql, + arguments, + ], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future delete( + String? table, { + String? where, + List? whereArgs, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [table], + { + #where: where, + #whereArgs: whereArgs, + }, + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i3.Batch batch() => (super.noSuchMethod( + Invocation.method( + #batch, + [], + ), + returnValue: _FakeBatch_6( + this, + Invocation.method( + #batch, + [], + ), + ), + ) as _i3.Batch); +} + +/// A class which mocks [Hub]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHub extends _i1.Mock implements _i2.Hub { + MockHub() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.SentryOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeSentryOptions_7( + this, + Invocation.getter(#options), + ), + ) as _i2.SentryOptions); + @override + bool get isEnabled => (super.noSuchMethod( + Invocation.getter(#isEnabled), + returnValue: false, + ) as bool); + @override + _i2.SentryId get lastEventId => (super.noSuchMethod( + Invocation.getter(#lastEventId), + returnValue: _FakeSentryId_8( + this, + Invocation.getter(#lastEventId), + ), + ) as _i2.SentryId); + @override + _i5.Future<_i2.SentryId> captureEvent( + _i2.SentryEvent? event, { + dynamic stackTrace, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureEvent, + [event], + { + #stackTrace: stackTrace, + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i5.Future<_i2.SentryId>.value(_FakeSentryId_8( + this, + Invocation.method( + #captureEvent, + [event], + { + #stackTrace: stackTrace, + #hint: hint, + #withScope: withScope, + }, + ), + )), + ) as _i5.Future<_i2.SentryId>); + @override + _i5.Future<_i2.SentryId> captureException( + dynamic throwable, { + dynamic stackTrace, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureException, + [throwable], + { + #stackTrace: stackTrace, + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i5.Future<_i2.SentryId>.value(_FakeSentryId_8( + this, + Invocation.method( + #captureException, + [throwable], + { + #stackTrace: stackTrace, + #hint: hint, + #withScope: withScope, + }, + ), + )), + ) as _i5.Future<_i2.SentryId>); + @override + _i5.Future<_i2.SentryId> captureMessage( + String? message, { + _i2.SentryLevel? level, + String? template, + List? params, + _i2.Hint? hint, + _i2.ScopeCallback? withScope, + }) => + (super.noSuchMethod( + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + returnValue: _i5.Future<_i2.SentryId>.value(_FakeSentryId_8( + this, + Invocation.method( + #captureMessage, + [message], + { + #level: level, + #template: template, + #params: params, + #hint: hint, + #withScope: withScope, + }, + ), + )), + ) as _i5.Future<_i2.SentryId>); + @override + _i5.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => + (super.noSuchMethod( + Invocation.method( + #captureUserFeedback, + [userFeedback], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addBreadcrumb( + _i2.Breadcrumb? crumb, { + _i2.Hint? hint, + }) => + (super.noSuchMethod( + Invocation.method( + #addBreadcrumb, + [crumb], + {#hint: hint}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + void bindClient(_i2.SentryClient? client) => super.noSuchMethod( + Invocation.method( + #bindClient, + [client], + ), + returnValueForMissingStub: null, + ); + @override + _i2.Hub clone() => (super.noSuchMethod( + Invocation.method( + #clone, + [], + ), + returnValue: _FakeHub_9( + this, + Invocation.method( + #clone, + [], + ), + ), + ) as _i2.Hub); + @override + _i5.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.FutureOr configureScope(_i2.ScopeCallback? callback) => + (super.noSuchMethod(Invocation.method( + #configureScope, + [callback], + )) as _i5.FutureOr); + @override + _i2.ISentrySpan startTransaction( + String? name, + String? operation, { + String? description, + DateTime? startTimestamp, + bool? bindToScope, + bool? waitForChildren, + Duration? autoFinishAfter, + bool? trimEnd, + _i2.OnTransactionFinish? onFinish, + Map? customSamplingContext, + }) => + (super.noSuchMethod( + Invocation.method( + #startTransaction, + [ + name, + operation, + ], + { + #description: description, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + #customSamplingContext: customSamplingContext, + }, + ), + returnValue: _i7.startTransactionShim( + name, + operation, + description: description, + startTimestamp: startTimestamp, + bindToScope: bindToScope, + waitForChildren: waitForChildren, + autoFinishAfter: autoFinishAfter, + trimEnd: trimEnd, + onFinish: onFinish, + customSamplingContext: customSamplingContext, + ), + ) as _i2.ISentrySpan); + @override + _i2.ISentrySpan startTransactionWithContext( + _i2.SentryTransactionContext? transactionContext, { + Map? customSamplingContext, + DateTime? startTimestamp, + bool? bindToScope, + bool? waitForChildren, + Duration? autoFinishAfter, + bool? trimEnd, + _i2.OnTransactionFinish? onFinish, + }) => + (super.noSuchMethod( + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + returnValue: _FakeISentrySpan_2( + this, + Invocation.method( + #startTransactionWithContext, + [transactionContext], + { + #customSamplingContext: customSamplingContext, + #startTimestamp: startTimestamp, + #bindToScope: bindToScope, + #waitForChildren: waitForChildren, + #autoFinishAfter: autoFinishAfter, + #trimEnd: trimEnd, + #onFinish: onFinish, + }, + ), + ), + ) as _i2.ISentrySpan); + @override + _i5.Future<_i2.SentryId> captureTransaction( + _i2.SentryTransaction? transaction, { + _i2.SentryTraceContextHeader? traceContext, + }) => + (super.noSuchMethod( + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext}, + ), + returnValue: _i5.Future<_i2.SentryId>.value(_FakeSentryId_8( + this, + Invocation.method( + #captureTransaction, + [transaction], + {#traceContext: traceContext}, + ), + )), + ) as _i5.Future<_i2.SentryId>); + @override + void setSpanContext( + dynamic throwable, + _i2.ISentrySpan? span, + String? transaction, + ) => + super.noSuchMethod( + Invocation.method( + #setSpanContext, + [ + throwable, + span, + transaction, + ], + ), + returnValueForMissingStub: null, + ); +} diff --git a/sqflite/test/sentry_batch_test.dart b/sqflite/test/sentry_batch_test.dart new file mode 100644 index 0000000000..78b6bf6796 --- /dev/null +++ b/sqflite/test/sentry_batch_test.dart @@ -0,0 +1,327 @@ +@TestOn('vm') + +import 'package:sentry/sentry.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sqflite/sqflite_dev.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import 'mocks/mocks.mocks.dart'; +import 'utils.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$SentryBatch success', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = SentrySqfliteDatabaseFactory( + databaseFactory: databaseFactoryFfi, + hub: fixture.hub, + ); + }); + + test('returns wrapped batch if performance enabled', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + expect(batch is SentryBatch, true); + + await db.close(); + }); + + test('returns original batch if performance disabled', () async { + final db = await fixture.getDatabase(tracesSampleRate: null); + final batch = db.batch(); + + expect(batch is! SentryBatch, true); + + await db.close(); + }); + + test('commit sets ok status', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('apply sets ok status', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.apply(); + + final span = fixture.tracer.children.last; + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates insert span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.insert('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect( + span.context.description, + 'INSERT INTO Product (title) VALUES (?)', + ); + + await db.close(); + }); + + test('creates raw insert span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawInsert('INSERT INTO Product (title) VALUES (?)', ['Product 1']); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect( + span.context.description, + 'INSERT INTO Product (title) VALUES (?)', + ); + + await db.close(); + }); + + test('creates update span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.update('Product', {'title': 'Product 1'}); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'UPDATE Product SET title = ?'); + + await db.close(); + }); + + test('creates raw update span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawUpdate('UPDATE Product SET title = ?', ['Product 1']); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'UPDATE Product SET title = ?'); + + await db.close(); + }); + + test('creates delete span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.delete('Product'); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'DELETE FROM Product'); + + await db.close(); + }); + + test('creates raw delete span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawDelete('DELETE FROM Product'); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'DELETE FROM Product'); + + await db.close(); + }); + + test('creates execute span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.execute('DELETE FROM Product'); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'DELETE FROM Product'); + + await db.close(); + }); + + test('creates query span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.query('Product'); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'SELECT * FROM Product'); + + await db.close(); + }); + + test('creates raw query span', () async { + final db = await fixture.getDatabase(); + final batch = db.batch(); + + batch.rawQuery('SELECT * FROM Product'); + + await batch.commit(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'SELECT * FROM Product'); + + await db.close(); + }); + + test('creates span 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 span = fixture.tracer.children.last; + + final desc = '''INSERT INTO Product (title) VALUES (?) +SELECT * FROM Product'''; + + expect(span.context.operation, 'db'); + expect(span.context.description, desc); + + await db.close(); + }); + + test('creates span 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 span = fixture.tracer.children.last; + + final desc = '''INSERT INTO Product (title) VALUES (?) +SELECT * FROM Product'''; + + expect(span.context.operation, 'db'); + expect(span.context.description, desc); + + await db.close(); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); + }); + + group('$SentryBatch fail', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + }); + + test('commit sets span 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'}); + + expect(() async => await batch.commit(), throwsException); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('apply sets span 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'}); + + expect(() async => await batch.apply(), throwsException); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + }); +} + +class Fixture { + final hub = MockHub(); + final options = SentryOptions(dsn: fakeDsn); + final _context = SentryTransactionContext('name', 'operation'); + late final tracer = SentryTracer(_context, hub); + final batch = MockBatch(); + final exception = Exception('error'); + + Future getDatabase({ + double? tracesSampleRate = 1.0, + }) async { + options.tracesSampleRate = tracesSampleRate; + final db = await openDatabase(inMemoryDatabasePath); + await db.execute(''' + CREATE TABLE Product ( + id INTEGER PRIMARY KEY, + title TEXT + )'''); + return db; + } +} diff --git a/sqflite/test/sentry_database_test.dart b/sqflite/test/sentry_database_test.dart new file mode 100644 index 0000000000..b4aa459db9 --- /dev/null +++ b/sqflite/test/sentry_database_test.dart @@ -0,0 +1,539 @@ +@TestOn('vm') + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; +import 'package:sentry_sqflite/src/sentry_database_executor.dart'; +import 'package:sentry_sqflite/src/version.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite_dev.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import 'mocks/mocks.mocks.dart'; + +import 'package:mockito/mockito.dart'; + +import 'utils.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$SentryDatabase success', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + test('adds integration', () async { + final db = await fixture.getSut(); + + expect( + fixture.options.sdk.integrations.contains('SentrySqfliteTracing'), + true, + ); + + await db.close(); + }); + + test('adds package', () async { + final db = await fixture.getSut(); + + expect( + fixture.options.sdk.packages.any( + (element) => + element.name == packageName && element.version == sdkVersion, + ), + true, + ); + + await db.close(); + }); + + test('creates close span', () async { + final db = await fixture.getSut(); + + await db.close(); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'Close DB: $inMemoryDatabasePath'); + expect(span.status, SpanStatus.ok()); + }); + + test('creates transaction span', () async { + final db = await fixture.getSut(); + + await db.transaction((txn) async { + expect(txn is SentrySqfliteTransaction, true); + }); + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.transaction'); + expect(span.context.description, 'Transaction DB: $inMemoryDatabasePath'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates transaction children run by the transaction', () async { + final db = await fixture.getSut(); + + await db.transaction((txn) async { + await txn.insert('Product', {'title': 'Product 1'}); + }); + final trSpan = fixture.tracer.children.first; + final insertSpan = fixture.tracer.children.last; + + expect(insertSpan.context.operation, 'db.sql.execute'); + expect( + insertSpan.context.description, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(insertSpan.context.parentSpanId, trSpan.context.spanId); + expect(insertSpan.status, SpanStatus.ok()); + + await db.close(); + }); + + test('transaction batch returns wrapped sentry batch', () async { + final db = await fixture.getSut(); + + await db.transaction((txn) async { + final batch = txn.batch(); + expect(batch is SentryBatch, true); + }); + + await db.close(); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); + }); + + group('$SentryDatabase fail', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + when(fixture.database.path).thenReturn('/path/db'); + when(fixture.database.execute(any)).thenAnswer((_) async => {}); + }); + + test('close sets span to internal error', () async { + when(fixture.database.close()).thenThrow(fixture.exception); + + final db = await fixture.getSut(database: fixture.database); + + expect(() async => await db.close(), throwsException); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('transaction sets span 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); + + expect(() async => await db.transaction((txn) async {}), throwsException); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + }); + + group('$SentryDatabaseExecutor success', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + test('creates delete span', () async { + final db = await fixture.getSut(); + + await db.delete('Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect(span.context.description, 'DELETE FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates execute span', () async { + final db = await fixture.getSut(); + + await db.execute('DELETE FROM Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect(span.context.description, 'DELETE FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates insert span', () async { + final db = await fixture.getSut(); + + await db.insert('Product', {'title': 'Product 1'}); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect( + span.context.description, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates query span', () async { + final db = await fixture.getSut(); + + await db.query('Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.query'); + expect(span.context.description, 'SELECT * FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates query cursor span', () async { + final db = await fixture.getSut(); + + await db.queryCursor('Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.query'); + expect(span.context.description, 'SELECT * FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates raw delete span', () async { + final db = await fixture.getSut(); + + await db.rawDelete('DELETE FROM Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect(span.context.description, 'DELETE FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates raw insert span', () async { + final db = await fixture.getSut(); + + await db + .rawInsert('INSERT INTO Product (title) VALUES (?)', ['Product 1']); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect( + span.context.description, + 'INSERT INTO Product (title) VALUES (?)', + ); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates raw query span', () async { + final db = await fixture.getSut(); + + await db.rawQuery('SELECT * FROM Product'); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.query'); + expect(span.context.description, 'SELECT * FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates raw query cursor span', () async { + final db = await fixture.getSut(); + + await db.rawQueryCursor('SELECT * FROM Product', []); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.query'); + expect(span.context.description, 'SELECT * FROM Product'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates raw update span', () async { + final db = await fixture.getSut(); + + await db.rawUpdate('UPDATE Product SET title = ?', ['Product 1']); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect(span.context.description, 'UPDATE Product SET title = ?'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + test('creates update span', () async { + final db = await fixture.getSut(); + + await db.update('Product', {'title': 'Product 1'}); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db.sql.execute'); + expect(span.context.description, 'UPDATE Product SET title = ?'); + expect(span.status, SpanStatus.ok()); + + await db.close(); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); + }); + + group('$SentryDatabaseExecutor fail', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + }); + + test('delete sets span to internal error', () async { + when(fixture.executor.delete(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.delete('Product'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('execute sets span to internal error', () async { + when(fixture.executor.execute(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.execute('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('insert sets span to internal error', () async { + when(fixture.executor.insert(any, any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor + .insert('Product', {'title': 'Product 1'}), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('query sets span to internal error', () async { + when(fixture.executor.query(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.query('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('query cursor sets span to internal error', () async { + when(fixture.executor.queryCursor(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.queryCursor('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('raw delete sets span to internal error', () async { + when(fixture.executor.rawDelete(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.rawDelete('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('raw insert sets span to internal error', () async { + when(fixture.executor.rawInsert(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.rawInsert('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('raw query sets span to internal error', () async { + when(fixture.executor.rawQuery(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.rawQuery('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('raw query cursor sets span to internal error', () async { + when(fixture.executor.rawQueryCursor(any, any)) + .thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.rawQueryCursor('sql', []), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('raw update sets span to internal error', () async { + when(fixture.executor.rawUpdate(any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor.rawUpdate('sql'), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + + test('update sets span to internal error', () async { + when(fixture.executor.update(any, any)).thenThrow(fixture.exception); + + final executor = fixture.getExecutorSut(); + + expect( + () async => await executor + .update('Product', {'title': 'Product 1'}), + throwsException, + ); + + final span = fixture.tracer.children.last; + expect(span.throwable, fixture.exception); + expect(span.status, SpanStatus.internalError()); + }); + }); +} + +class Fixture { + final hub = MockHub(); + final options = SentryOptions(dsn: fakeDsn); + final _context = SentryTransactionContext('name', 'operation'); + late final tracer = SentryTracer(_context, hub); + final database = MockDatabase(); + final exception = Exception('error'); + final executor = MockDatabaseExecutor(); + + Future getSut({ + double? tracesSampleRate = 1.0, + Database? database, + bool execute = true, + }) async { + options.tracesSampleRate = tracesSampleRate; + final db = database ?? await openDatabase(inMemoryDatabasePath); + if (execute) { + await db.execute(''' + CREATE TABLE Product ( + id INTEGER PRIMARY KEY, + title TEXT + )'''); + } + return SentryDatabase(db, hub: hub); + } + + SentryDatabaseExecutor getExecutorSut() { + return SentryDatabaseExecutor(executor, hub: hub); + } +} diff --git a/sqflite/test/sentry_sqflite_database_factory_dart_test.dart b/sqflite/test/sentry_sqflite_database_factory_dart_test.dart new file mode 100644 index 0000000000..1926ad68d5 --- /dev/null +++ b/sqflite/test/sentry_sqflite_database_factory_dart_test.dart @@ -0,0 +1,77 @@ +@TestOn('vm') + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite_dev.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import 'mocks/mocks.mocks.dart'; + +import 'package:mockito/mockito.dart'; + +import 'utils.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('openDatabase', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = SentrySqfliteDatabaseFactory( + databaseFactory: databaseFactoryFfi, + hub: fixture.hub, + ); + }); + + test('returns wrapped data base if performance enabled', () async { + final db = await openDatabase(inMemoryDatabasePath); + + expect(db is SentryDatabase, true); + + await db.close(); + }); + + test('returns original data base if performance disabled', () async { + fixture.options.tracesSampleRate = null; + + final db = await openDatabase(inMemoryDatabasePath); + + expect(db is! SentryDatabase, true); + + await db.close(); + }); + + test('starts and finishes a open db span when performance enabled', + () async { + final db = await openDatabase(inMemoryDatabasePath); + + final span = fixture.tracer.children.last; + expect(span.context.operation, 'db'); + expect(span.context.description, 'Open DB: $inMemoryDatabasePath'); + + await db.close(); + }); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); +} + +class Fixture { + final hub = MockHub(); + final options = SentryOptions(dsn: fakeDsn)..tracesSampleRate = 1.0; + final _context = SentryTransactionContext('name', 'operation'); + late final tracer = SentryTracer(_context, hub); +} diff --git a/sqflite/test/sentry_sqflite_test.dart b/sqflite/test/sentry_sqflite_test.dart new file mode 100644 index 0000000000..5bd099a063 --- /dev/null +++ b/sqflite/test/sentry_sqflite_test.dart @@ -0,0 +1,96 @@ +@TestOn('vm') + +import 'package:sentry/sentry.dart'; +import 'package:sentry_sqflite/sentry_sqflite.dart'; +import 'package:sentry_sqflite/src/sentry_sqflite.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite/sqflite_dev.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +import 'mocks/mocks.mocks.dart'; +import 'utils.dart'; + +import 'package:mockito/mockito.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('openDatabaseWithSentry', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + test('returns wrapped data base if performance enabled', () async { + final db = + await openDatabaseWithSentry(inMemoryDatabasePath, hub: fixture.hub); + + expect(db is SentryDatabase, true); + + await db.close(); + }); + + test('returns wrapped read only data base if performance enabled ', + () async { + final db = await openReadOnlyDatabaseWithSentry( + inMemoryDatabasePath, + hub: fixture.hub, + ); + + expect(db is SentryDatabase, true); + + await db.close(); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); + }); + + group('openReadOnlyDatabaseWithSentry', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + + when(fixture.hub.options).thenReturn(fixture.options); + when(fixture.hub.getSpan()).thenReturn(fixture.tracer); + + // using ffi for testing on vm + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + test('returns wrapped data base if performance enabled', () async { + final db = await openReadOnlyDatabaseWithSentry( + inMemoryDatabasePath, + hub: fixture.hub, + ); + + expect(db is SentryDatabase, true); + + await db.close(); + }); + + tearDown(() { + databaseFactory = sqfliteDatabaseFactoryDefault; + }); + }); +} + +class Fixture { + final hub = MockHub(); + final options = SentryOptions(dsn: fakeDsn)..tracesSampleRate = 1.0; + final _context = SentryTransactionContext('name', 'operation'); + late final tracer = SentryTracer(_context, hub); +} diff --git a/sqflite/test/utils.dart b/sqflite/test/utils.dart new file mode 100644 index 0000000000..5dbeee585a --- /dev/null +++ b/sqflite/test/utils.dart @@ -0,0 +1 @@ +final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; diff --git a/sqflite/test/version_test.dart b/sqflite/test/version_test.dart new file mode 100644 index 0000000000..fbe3aa0a93 --- /dev/null +++ b/sqflite/test/version_test.dart @@ -0,0 +1,18 @@ +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry_sqflite/src/version.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yaml/yaml.dart' as yaml; + +void main() { + test( + 'sdkVersion matches that of pubspec.yaml', + () { + final dynamic pubspec = + yaml.loadYaml(File('pubspec.yaml').readAsStringSync()); + expect(sdkVersion, pubspec['version']); + }, + ); +}