From b47809a5f032149d70c9c8cdc226a1b71189ae88 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Tue, 6 Dec 2022 15:18:03 +0100 Subject: [PATCH] Tracing for File IO integration (#1160) --- .craft.yml | 2 + .github/workflows/file.yml | 64 +++ .github/workflows/min_version_test.yml | 2 +- .gitignore | 1 + CHANGELOG.md | 4 + README.md | 3 +- file/CHANGELOG.md | 1 + file/LICENSE | 21 + file/README.md | 79 ++++ file/analysis_options.yaml | 23 + file/example/example.dart | 51 ++ file/lib/sentry_file.dart | 2 + file/lib/src/sentry_file.dart | 387 +++++++++++++++ file/lib/src/sentry_file_extension.dart | 43 ++ file/lib/src/version.dart | 2 + file/pubspec.yaml | 20 + file/pubspec_overrides.yaml | 3 + file/test/mock_platform_checker.dart | 11 + file/test/mock_sentry_client.dart | 27 ++ file/test/no_such_method_provider.dart | 7 + file/test/sentry_file_extension_test.dart | 64 +++ file/test/sentry_file_test.dart | 546 ++++++++++++++++++++++ file/test/version_test.dart | 19 + file/test_resources/sentry.png | Bin 0 -> 3535 bytes file/test_resources/testfile.txt | 1 + flutter/example/pubspec.yaml | 1 + min_version_test/lib/main.dart | 60 ++- min_version_test/pubspec.yaml | 3 + scripts/bump-version.sh | 2 +- 29 files changed, 1436 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/file.yml create mode 120000 file/CHANGELOG.md create mode 100644 file/LICENSE create mode 100644 file/README.md create mode 100644 file/analysis_options.yaml create mode 100644 file/example/example.dart create mode 100644 file/lib/sentry_file.dart create mode 100644 file/lib/src/sentry_file.dart create mode 100644 file/lib/src/sentry_file_extension.dart create mode 100644 file/lib/src/version.dart create mode 100644 file/pubspec.yaml create mode 100644 file/pubspec_overrides.yaml create mode 100644 file/test/mock_platform_checker.dart create mode 100644 file/test/mock_sentry_client.dart create mode 100644 file/test/no_such_method_provider.dart create mode 100644 file/test/sentry_file_extension_test.dart create mode 100644 file/test/sentry_file_test.dart create mode 100644 file/test/version_test.dart create mode 100644 file/test_resources/sentry.png create mode 100644 file/test_resources/testfile.txt diff --git a/.craft.yml b/.craft.yml index 70e6fb387..0967399ec 100644 --- a/.craft.yml +++ b/.craft.yml @@ -9,6 +9,7 @@ targets: flutter: logging: dio: + file: - name: github - name: registry sdks: @@ -16,3 +17,4 @@ targets: pub:sentry_flutter: pub:sentry_logging: pub:sentry_dio: + #pub:sentry_file: diff --git a/.github/workflows/file.yml b/.github/workflows/file.yml new file mode 100644 index 000000000..3889deac2 --- /dev/null +++ b/.github/workflows/file.yml @@ -0,0 +1,64 @@ +name: sentry-file +on: + push: + branches: + - main + - release/** + pull_request: + paths-ignore: + - 'logging/**' + - 'flutter/**' + - 'dio/**' + +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: Build ${{matrix.sdk}} on ${{matrix.os}} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + working-directory: ./file + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + # removing beta because of Dart 2.19.0 + sdk: [stable] + steps: + - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d # pin@v1 + with: + sdk: ${{ matrix.sdk }} + - uses: actions/checkout@v3 + + - name: Test VM + run: | + dart pub get + dart test -p vm --coverage=coverage --test-randomize-ordering-seed=random --chain-stack-traces + dart pub run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=lib + + - uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # pin@v3 + if: runner.os == 'Linux' && matrix.sdk == 'stable' + with: + name: sentry_file + files: ./file/coverage/lcov.info + + - uses: VeryGoodOpenSource/very_good_coverage@84e5b54ab888644554e5573dca87d7f76dec9fb3 # pin@v2.0.0 + if: runner.os == 'Linux' && matrix.sdk == 'stable' + with: + path: './file/coverage/lcov.info' + min_coverage: 55 + + analyze: + uses: ./.github/workflows/analyze.yml + with: + package: file + panaThreshold: 90 diff --git a/.github/workflows/min_version_test.yml b/.github/workflows/min_version_test.yml index dcd8ad3f2..a39e13a5a 100644 --- a/.github/workflows/min_version_test.yml +++ b/.github/workflows/min_version_test.yml @@ -31,7 +31,7 @@ jobs: - uses: subosito/flutter-action@dbf1fa04f4d2e52c33185153d06cdb5443aa189d # pin@v2 with: flutter-version: '2.0.0' - + # Add flutter build web (missing index) - name: Build run: | cd min_version_test diff --git a/.gitignore b/.gitignore index b2807fa8e..742ddcded 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ build/ dart/coverage/* logging/coverage/* dio/coverage/* +file/coverage/* pubspec.lock Podfile.lock flutter/coverage/* diff --git a/CHANGELOG.md b/CHANGELOG.md index b996ec1a7..c704a9180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Tracing for File IO integration ([#1160](https://github.com/getsentry/sentry-dart/pull/1160)) + ### Dependencies - Bump Cocoa SDK from v7.31.2 to v7.31.3 ([#1157](https://github.com/getsentry/sentry-dart/pull/1157)) diff --git a/README.md b/README.md index cb5d8f991..86a986819 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ Sentry SDK for Dart and Flutter | sentry_flutter | [![build](https://github.com/getsentry/sentry-dart/workflows/sentry-flutter/badge.svg?branch=main)](https://github.com/getsentry/sentry-dart/actions?query=workflow%3Asentry-flutter) | [![pub package](https://img.shields.io/pub/v/sentry_flutter.svg)](https://pub.dev/packages/sentry_flutter) | [![likes](https://img.shields.io/pub/likes/sentry_flutter?logo=dart)](https://pub.dev/packages/sentry_flutter/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_flutter?logo=dart)](https://pub.dev/packages/sentry_flutter/score) | [![pub points](https://img.shields.io/pub/points/sentry_flutter?logo=dart)](https://pub.dev/packages/sentry_flutter/score) | 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) ##### Usage -For detailed usage, check out the inner [dart](https://github.com/getsentry/sentry-dart/tree/main/dart), [flutter](https://github.com/getsentry/sentry-dart/tree/main/flutter), [logging](https://github.com/getsentry/sentry-dart/tree/main/logging) and [dio](https://github.com/getsentry/sentry-dart/tree/main/dio) `README's` or our `Resources` section below. +For detailed usage, check out the inner [dart](https://github.com/getsentry/sentry-dart/tree/main/dart), [flutter](https://github.com/getsentry/sentry-dart/tree/main/flutter), [logging](https://github.com/getsentry/sentry-dart/tree/main/logging), [dio](https://github.com/getsentry/sentry-dart/tree/main/dio) and [file](https://github.com/getsentry/sentry-dart/tree/main/file) `README's` or our `Resources` section below. #### Blog posts diff --git a/file/CHANGELOG.md b/file/CHANGELOG.md new file mode 120000 index 000000000..04c99a55c --- /dev/null +++ b/file/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/file/LICENSE b/file/LICENSE new file mode 100644 index 000000000..2a6964d84 --- /dev/null +++ b/file/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/file/README.md b/file/README.md new file mode 100644 index 000000000..700706d41 --- /dev/null +++ b/file/README.md @@ -0,0 +1,79 @@ +

+ + + +
+

+ +Sentry integration for `dart.io.File` +=========== + +| package | build | pub | likes | popularity | pub points | +| ------- | ------- | ------- | ------- | ------- | ------- | +| 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)](https://pub.dev/packages/sentry_file/score) | [![popularity](https://img.shields.io/pub/popularity/sentry_file)](https://pub.dev/packages/sentry_file/score) | [![pub points](https://img.shields.io/pub/points/sentry_file)](https://pub.dev/packages/sentry_file/score) + +#### 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. + +- [Set Up](https://docs.sentry.io/platforms/dart/performance/) Performance. + +```dart +import 'package:sentry/sentry.dart'; +import 'package:sentry_file/sentry_file.dart'; +import 'dart:io'; + +Future main() async { + // or SentryFlutter.init + await Sentry.init( + (options) { + options.dsn = 'https://example@sentry.io/example'; + // To set a uniform sample rate + options.tracesSampleRate = 1.0; + }, + appRunner: runApp, // Init your App. + ); +} + +Future runApp() async { + final file = File('my_file.txt'); + // Call the Sentry extension method to wrap up the File + final sentryFile = file.sentryTrace(); + + // Start a transaction if there's no active transaction + final transaction = Sentry.startTransaction( + 'MyFileExample', + 'file', + bindToScope: true, + ); + + // create the File + await sentryFile.create(); + // Write some content + await sentryFile.writeAsString('Hello World'); + // Read the content + final text = await sentryFile.readAsString(); + + print(text); + + // Delete the file + await sentryFile.delete(); + + // Finish the transaction + await transaction.finish(status: SpanStatus.ok()); + + await Sentry.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/file/analysis_options.yaml b/file/analysis_options.yaml new file mode 100644 index 000000000..3dd01b96b --- /dev/null +++ b/file/analysis_options.yaml @@ -0,0 +1,23 @@ +include: package:lints/recommended.yaml + +analyzer: + exclude: + - example/** # the example has its own 'analysis_options.yaml' + 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 + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + prefer_relative_imports: true + unnecessary_brace_in_string_interps: true + prefer_function_declarations_over_variables: false + no_leading_underscores_for_local_identifiers: false + avoid_renaming_method_parameters: false + unawaited_futures: true diff --git a/file/example/example.dart b/file/example/example.dart new file mode 100644 index 000000000..1423efe4a --- /dev/null +++ b/file/example/example.dart @@ -0,0 +1,51 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry_file/sentry_file.dart'; +import 'dart:io'; + +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'; + + // or SentryFlutter.init + await Sentry.init( + (options) { + options.dsn = dsn; + // To capture the absolute path of the file + options.sendDefaultPii = true; + // To set a uniform sample rate + options.tracesSampleRate = 1.0; + }, + appRunner: runApp, // Init your App. + ); +} + +Future runApp() async { + final file = File('my_file.txt'); + // Call the Sentry extension method to wrap up the File + final sentryFile = file.sentryTrace(); + + // Start a transaction if there's no active transaction + final transaction = Sentry.startTransaction( + 'MyFileExample', + 'file', + bindToScope: true, + ); + + // Create the File + await sentryFile.create(); + // Write some content + await sentryFile.writeAsString('Hello World'); + // Read the content + final text = await sentryFile.readAsString(); + + print(text); + + // Delete the file + await sentryFile.delete(); + + // Finish the transaction + await transaction.finish(status: SpanStatus.ok()); + + await Sentry.close(); +} diff --git a/file/lib/sentry_file.dart b/file/lib/sentry_file.dart new file mode 100644 index 000000000..bd59fdea5 --- /dev/null +++ b/file/lib/sentry_file.dart @@ -0,0 +1,2 @@ +export 'src/sentry_file.dart'; +export 'src/sentry_file_extension.dart'; diff --git a/file/lib/src/sentry_file.dart b/file/lib/src/sentry_file.dart new file mode 100644 index 000000000..36cd88034 --- /dev/null +++ b/file/lib/src/sentry_file.dart @@ -0,0 +1,387 @@ +// Adapted from https://github.com/ueman/sentry-dart-tools/blob/8e41418c0f2c62dc88292cf32a4f22e79112b744/sentry_plus/lib/src/file/sentry_file.dart + +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; + +typedef Callback = FutureOr Function(); + +/// The Sentry wrapper for the File IO implementation that creates a span +/// out of the active transaction in the scope. +/// The span is started before the operation is executed and finished after. +/// The File tracing isn't available for Web. +/// +/// Example: +/// +/// ```dart +/// import 'dart:io'; +/// +/// final file = File('test.txt'); +/// final sentryFile = SentryFile(file); +/// // span starts +/// await sentryFile.writeAsString('Hello World'); +/// // span finishes +/// ``` +/// +/// All the copy, create, delete, open, rename, read, and write operations are +/// supported. +@experimental +class SentryFile implements File { + SentryFile( + this._file, { + @internal Hub? hub, + }) : _hub = hub ?? HubAdapter() { + _hub.options.sdk.addIntegration('SentryFileTracing'); + } + + final File _file; + final Hub _hub; + + @override + Future copy(String newPath) { + return _wrap(() async => _file.copy(newPath), 'file.copy'); + } + + @override + File copySync(String newPath) { + return _wrapSync(() => _file.copySync(newPath), 'file.copy'); + } + + @override + Future create({bool recursive = false}) { + return _wrap( + () async => _file.create(recursive: recursive), + 'file.write', + ); + } + + @override + void createSync({bool recursive = false, bool exclusive = false}) { + return _wrapSync( + () => _file.createSync(recursive: recursive), + 'file.write', + ); + } + + @override + Future delete({bool recursive = false}) { + return _wrap(() async => _file.delete(recursive: recursive), 'file.delete'); + } + + @override + void deleteSync({bool recursive = false}) { + _wrapSync(() => _file.deleteSync(recursive: recursive), 'file.delete'); + } + + @override + Future open({FileMode mode = FileMode.read}) { + return _wrap(() async => _file.open(mode: mode), 'file.open'); + } + + // coverage:ignore-start + + @override + Stream> openRead([int? start, int? end]) { + return _file.openRead(start, end); + } + + @override + RandomAccessFile openSync({FileMode mode = FileMode.read}) { + return _file.openSync(mode: mode); + } + + @override + IOSink openWrite({FileMode mode = FileMode.write, Encoding encoding = utf8}) { + return _file.openWrite(mode: mode, encoding: encoding); + } + + // coverage:ignore-end + + @override + Future readAsBytes() { + return _wrap(() async => _file.readAsBytes(), 'file.read'); + } + + @override + Uint8List readAsBytesSync() { + return _wrapSync(() => _file.readAsBytesSync(), 'file.read'); + } + + @override + Future> readAsLines({Encoding encoding = utf8}) { + return _wrap( + () async => _file.readAsLines(encoding: encoding), 'file.read'); + } + + @override + List readAsLinesSync({Encoding encoding = utf8}) { + return _wrapSync( + () => _file.readAsLinesSync(encoding: encoding), + 'file.read', + ); + } + + @override + Future readAsString({Encoding encoding = utf8}) { + return _wrap( + () async => _file.readAsString(encoding: encoding), 'file.read'); + } + + @override + String readAsStringSync({Encoding encoding = utf8}) { + return _wrapSync( + () => _file.readAsStringSync(encoding: encoding), + 'file.read', + ); + } + + @override + Future rename(String newPath) { + return _wrap(() async => _file.rename(newPath), 'file.rename'); + } + + @override + File renameSync(String newPath) { + return _wrapSync(() => _file.renameSync(newPath), 'file.rename'); + } + + @override + Future writeAsBytes( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) { + return _wrap( + () async => _file.writeAsBytes(bytes, mode: mode, flush: flush), + 'file.write', + ); + } + + @override + void writeAsBytesSync( + List bytes, { + FileMode mode = FileMode.write, + bool flush = false, + }) { + _wrapSync( + () => _file.writeAsBytesSync(bytes, mode: mode, flush: flush), + 'file.write', + ); + } + + @override + Future writeAsString( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) { + return _wrap( + () async => _file.writeAsString( + contents, + mode: mode, + encoding: encoding, + flush: flush, + ), + 'file.write', + ); + } + + @override + void writeAsStringSync( + String contents, { + FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false, + }) { + _wrapSync( + () => _file.writeAsStringSync( + contents, + mode: mode, + encoding: encoding, + flush: flush, + ), + 'file.write', + ); + } + + String _getDesc() { + return uri.pathSegments.isNotEmpty ? uri.pathSegments.last : path; + } + + Future _wrap(Callback callback, String operation) async { + final desc = _getDesc(); + + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild(operation, description: desc); + + span?.setData('file.async', true); + if (_hub.options.sendDefaultPii) { + span?.setData('file.path', absolute.path); + } + T data; + try { + // workaround for having the length when the file does not exist + // or its being deleted. + int? length; + var hasLength = false; + try { + length = await _file.length(); + hasLength = true; + } catch (_) { + // ignore in case something goes wrong + } + + data = await callback(); + + if (!hasLength) { + try { + length = await _file.length(); + } catch (_) { + // ignore in case something goes wrong + } + } + + if (length != null) { + span?.setData('file.size', length); + } + + span?.status = SpanStatus.ok(); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + rethrow; + } finally { + await span?.finish(); + } + return data; + } + + T _wrapSync(Callback callback, String operation) { + final desc = _getDesc(); + + final currentSpan = _hub.getSpan(); + final span = currentSpan?.startChild(operation, description: desc); + span?.setData('file.async', false); + + if (_hub.options.sendDefaultPii) { + span?.setData('file.path', absolute.path); + } + + T data; + try { + // workaround for having the length when the file does not exist + // or its being deleted. + int? length; + var hasLength = false; + try { + length = _file.lengthSync(); + hasLength = true; + } catch (_) { + // ignore in case something goes wrong + } + + data = callback() as T; + + if (!hasLength) { + try { + length = _file.lengthSync(); + } catch (_) { + // ignore in case something goes wrong + } + } + + if (length != null) { + span?.setData('file.size', length); + } + + span?.status = SpanStatus.ok(); + } catch (exception) { + span?.throwable = exception; + span?.status = SpanStatus.internalError(); + rethrow; + } finally { + span?.finish(); + } + return data; + } + + // coverage:ignore-start + + @override + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) => + _file.watch(events: events, recursive: recursive); + + @override + Future resolveSymbolicLinks() => _file.resolveSymbolicLinks(); + + @override + String resolveSymbolicLinksSync() => _file.resolveSymbolicLinksSync(); + + @override + Future setLastAccessed(DateTime time) => _file.setLastAccessed(time); + + @override + void setLastAccessedSync(DateTime time) => _file.setLastAccessedSync(time); + + @override + Future setLastModified(DateTime time) => _file.setLastModified(time); + + @override + void setLastModifiedSync(DateTime time) => _file.setLastAccessedSync(time); + + @override + Directory get parent => _file.parent; + + @override + String get path => _file.path; + + @override + File get absolute => _file.absolute; + + @override + Future exists() => _file.exists(); + + @override + bool existsSync() => _file.existsSync(); + + @override + bool get isAbsolute => _file.isAbsolute; + + @override + Future lastAccessed() => _file.lastAccessed(); + + @override + DateTime lastAccessedSync() => _file.lastAccessedSync(); + + @override + Future lastModified() => _file.lastModified(); + + @override + DateTime lastModifiedSync() => _file.lastModifiedSync(); + + @override + Future length() => _file.length(); + + @override + int lengthSync() => _file.lengthSync(); + + @override + Future stat() => _file.stat(); + + @override + FileStat statSync() => _file.statSync(); + + @override + Uri get uri => _file.uri; + + // coverage:ignore-end +} diff --git a/file/lib/src/sentry_file_extension.dart b/file/lib/src/sentry_file_extension.dart new file mode 100644 index 000000000..2c2f2657f --- /dev/null +++ b/file/lib/src/sentry_file_extension.dart @@ -0,0 +1,43 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:io' if (dart.library.html) 'dart:html'; + +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; + +import '../sentry_file.dart'; + +extension SentryFileExtension on File { + /// The Sentry wrapper for the File IO implementation that creates a span + /// out of the active transaction in the scope. + /// The span is started before the operation is executed and finished after. + /// The File tracing isn't available for Web. + /// + /// Example: + /// + /// ```dart + /// import 'dart:io'; + /// + /// final file = File('test.txt'); + /// final sentryFile = SentryFile(file); + /// // span starts + /// await sentryFile.writeAsString('Hello World'); + /// // span finishes + /// ``` + /// + /// All the copy, create, delete, open, rename, read, and write operations are + /// supported. + @experimental + File sentryTrace({ + @internal Hub? hub, + }) { + final _hub = hub ?? HubAdapter(); + + if (_hub.options.platformChecker.isWeb || + !_hub.options.isTracingEnabled()) { + return this; + } + + return SentryFile(this, hub: _hub); + } +} diff --git a/file/lib/src/version.dart b/file/lib/src/version.dart new file mode 100644 index 000000000..e8297f135 --- /dev/null +++ b/file/lib/src/version.dart @@ -0,0 +1,2 @@ +/// The SDK version reported to Sentry.io in the submitted events. +const String sdkVersion = '6.17.0'; diff --git a/file/pubspec.yaml b/file/pubspec.yaml new file mode 100644 index 000000000..fe353892c --- /dev/null +++ b/file/pubspec.yaml @@ -0,0 +1,20 @@ +name: sentry_file +description: An integration which adds support for performance tracing for dart.io.File. +version: 6.17.0 +homepage: https://docs.sentry.io/platforms/dart/ +repository: https://github.com/getsentry/sentry-dart +issue_tracker: https://github.com/getsentry/sentry-dart/issues + +environment: + # <2.19 because of https://github.com/dart-lang/sdk/issues/49647 breaking change + sdk: '>=2.12.0 <2.19.0' + +dependencies: + sentry: 6.17.0 + meta: ^1.3.0 + +dev_dependencies: + lints: ^2.0.0 + test: ^1.21.1 + coverage: ^1.3.0 + mockito: ^5.1.0 diff --git a/file/pubspec_overrides.yaml b/file/pubspec_overrides.yaml new file mode 100644 index 000000000..16e71d16f --- /dev/null +++ b/file/pubspec_overrides.yaml @@ -0,0 +1,3 @@ +dependency_overrides: + sentry: + path: ../dart diff --git a/file/test/mock_platform_checker.dart b/file/test/mock_platform_checker.dart new file mode 100644 index 000000000..6dd95a5c0 --- /dev/null +++ b/file/test/mock_platform_checker.dart @@ -0,0 +1,11 @@ +import 'no_such_method_provider.dart'; +import 'package:sentry/src/platform_checker.dart'; + +class MockPlatformChecker extends PlatformChecker with NoSuchMethodProvider { + MockPlatformChecker(this._isWeb); + + final bool _isWeb; + + @override + bool get isWeb => _isWeb; +} diff --git a/file/test/mock_sentry_client.dart b/file/test/mock_sentry_client.dart new file mode 100644 index 000000000..c68fde9b4 --- /dev/null +++ b/file/test/mock_sentry_client.dart @@ -0,0 +1,27 @@ +import 'package:sentry/sentry.dart'; + +import 'no_such_method_provider.dart'; + +final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; + +class MockSentryClient with NoSuchMethodProvider implements SentryClient { + List captureTransactionCalls = []; + + @override + Future captureTransaction( + SentryTransaction transaction, { + Scope? scope, + SentryTraceContextHeader? traceContext, + }) async { + captureTransactionCalls + .add(CaptureTransactionCall(transaction, traceContext)); + return transaction.eventId; + } +} + +class CaptureTransactionCall { + final SentryTransaction transaction; + final SentryTraceContextHeader? traceContext; + + CaptureTransactionCall(this.transaction, this.traceContext); +} diff --git a/file/test/no_such_method_provider.dart b/file/test/no_such_method_provider.dart new file mode 100644 index 000000000..64253e965 --- /dev/null +++ b/file/test/no_such_method_provider.dart @@ -0,0 +1,7 @@ +mixin NoSuchMethodProvider { + @override + void noSuchMethod(Invocation invocation) { + 'Method ${invocation.memberName} was called ' + 'with arguments ${invocation.positionalArguments}'; + } +} diff --git a/file/test/sentry_file_extension_test.dart b/file/test/sentry_file_extension_test.dart new file mode 100644 index 000000000..73eb94bef --- /dev/null +++ b/file/test/sentry_file_extension_test.dart @@ -0,0 +1,64 @@ +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry_file/sentry_file.dart'; +import 'package:test/test.dart'; + +import 'mock_platform_checker.dart'; +import 'mock_sentry_client.dart'; + +void main() { + group('$File extension', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('io performance enabled wraps file', () async { + final sut = fixture.getSut( + tracesSampleRate: 1.0, + ); + + expect(sut is SentryFile, true); + }); + + test('io performance disabled does not wrap file', () async { + final sut = fixture.getSut( + tracesSampleRate: null, + ); + + expect(sut is SentryFile, false); + }); + + test('web does not wrap file', () async { + final sut = fixture.getSut( + tracesSampleRate: 1.0, + isWeb: true, + ); + + expect(sut is SentryFile, false); + }); + }); +} + +class Fixture { + final options = SentryOptions(dsn: fakeDsn); + late Hub hub; + + File getSut({ + double? tracesSampleRate, + bool isWeb = false, + }) { + options.tracesSampleRate = tracesSampleRate; + options.platformChecker = MockPlatformChecker(isWeb); + + hub = Hub(options); + + final file = File('test_resources/testfile.txt'); + + return file.sentryTrace(hub: hub); + } +} diff --git a/file/test/sentry_file_test.dart b/file/test/sentry_file_test.dart new file mode 100644 index 000000000..74b10ac8c --- /dev/null +++ b/file/test/sentry_file_test.dart @@ -0,0 +1,546 @@ +// ignore_for_file: invalid_use_of_internal_member + +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry_file/sentry_file.dart'; +import 'package:test/test.dart'; + +import 'mock_sentry_client.dart'; + +typedef Callback = T Function(); + +void main() { + group('$SentryFile copy', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(bool async) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.copy'); + expect(span.data['file.size'], 7); + expect(span.data['file.async'], async); + expect(span.context.description, 'testfile.txt'); + expect( + (span.data['file.path'] as String) + .endsWith('test_resources/testfile.txt'), + true); + } + + test('async', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = await sut.copy('test_resources/testfile_copy.txt'); + + await tr.finish(); + + expect(await newFile.exists(), true); + + expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); + + _assertSpan(true); + + await newFile.delete(); + }); + + test('sync', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = sut.copySync('test_resources/testfile_copy.txt'); + + await tr.finish(); + + expect(newFile.existsSync(), true); + + expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); + + _assertSpan(false); + + newFile.deleteSync(); + }); + }); + + group('$SentryFile create', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(bool async, {int? size = 0}) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.write'); + expect(span.data['file.size'], size); + expect(span.data['file.async'], async); + expect(span.context.description, 'testfile_create.txt'); + expect( + (span.data['file.path'] as String) + .endsWith('test_resources/testfile_create.txt'), + true); + } + + test('async', () async { + final file = File('test_resources/testfile_create.txt'); + expect(await file.exists(), false); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = await sut.create(); + + await tr.finish(); + + expect(await newFile.exists(), true); + + _assertSpan(true); + + await newFile.delete(); + }); + + test('sync', () async { + final file = File('test_resources/testfile_create.txt'); + expect(await file.exists(), false); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.createSync(); + + await tr.finish(); + + expect(sut.existsSync(), true); + + _assertSpan(false); + + sut.deleteSync(); + }); + }); + + group('$SentryFile delete', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(bool async, {int? size = 0}) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.delete'); + expect(span.data['file.size'], size); + expect(span.data['file.async'], async); + expect(span.context.description, 'testfile_delete.txt'); + expect( + (span.data['file.path'] as String) + .endsWith('test_resources/testfile_delete.txt'), + true); + } + + test('async', () async { + final file = File('test_resources/testfile_delete.txt'); + await file.create(); + expect(await file.exists(), true); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = await sut.delete(); + + await tr.finish(); + + expect(await newFile.exists(), false); + + _assertSpan(true); + }); + + test('sync', () async { + final file = File('test_resources/testfile_delete.txt'); + file.createSync(); + expect(file.existsSync(), true); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.deleteSync(); + + await tr.finish(); + + expect(sut.existsSync(), false); + + _assertSpan(false); + }); + }); + + group('$SentryFile open', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan() { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.open'); + expect(span.data['file.size'], 3535); + expect(span.data['file.async'], true); + expect(span.context.description, 'sentry.png'); + expect( + (span.data['file.path'] as String) + .endsWith('test_resources/sentry.png'), + true); + } + + test('async', () async { + final file = File('test_resources/sentry.png'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = await sut.open(); + + await tr.finish(); + + await newFile.close(); + + _assertSpan(); + }); + }); + + group('$SentryFile read', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(String fileName, bool async, {int? size = 0}) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.read'); + expect(span.data['file.size'], size); + expect(span.data['file.async'], async); + expect(span.context.description, fileName); + expect( + (span.data['file.path'] as String) + .endsWith('test_resources/$fileName'), + true); + } + + test('as bytes async', () async { + final file = File('test_resources/sentry.png'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + await sut.readAsBytes(); + + await tr.finish(); + + _assertSpan('sentry.png', true, size: 3535); + }); + + test('as bytes sync', () async { + final file = File('test_resources/sentry.png'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.readAsBytesSync(); + + await tr.finish(); + + _assertSpan('sentry.png', false, size: 3535); + }); + + test('lines async', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + await sut.readAsLines(); + + await tr.finish(); + + _assertSpan('testfile.txt', true, size: 7); + }); + + test('lines sync', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.readAsLinesSync(); + + await tr.finish(); + + _assertSpan('testfile.txt', false, size: 7); + }); + + test('string async', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + await sut.readAsString(); + + await tr.finish(); + + _assertSpan('testfile.txt', true, size: 7); + }); + + test('string sync', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.readAsStringSync(); + + await tr.finish(); + + _assertSpan('testfile.txt', false, size: 7); + }); + }); + + group('$SentryFile rename', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(bool async, String name) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.context.operation, 'file.rename'); + expect(span.data['file.size'], 0); + expect(span.data['file.async'], async); + expect(span.context.description, name); + expect( + (span.data['file.path'] as String).endsWith('test_resources/$name'), + true); + } + + test('async', () async { + final file = File('test_resources/old_name.txt'); + await file.create(); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = await sut.rename('test_resources/new_name.txt'); + + await tr.finish(); + + expect(await file.exists(), false); + expect(await newFile.exists(), true); + + expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); + + _assertSpan(true, 'old_name.txt'); + + await newFile.delete(); + }); + + test('sync', () async { + final file = File('test_resources/old_name.txt'); + file.createSync(); + + final sut = fixture.getSut( + file, + sendDefaultPii: true, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + final newFile = sut.renameSync('test_resources/testfile_copy.txt'); + + await tr.finish(); + + expect(file.existsSync(), false); + expect(newFile.existsSync(), true); + + expect(sut.uri.toFilePath(), isNot(newFile.uri.toFilePath())); + + _assertSpan(false, 'old_name.txt'); + + newFile.deleteSync(); + }); + }); + + group('$SentryOptions config', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void _assertSpan(bool async) { + final call = fixture.client.captureTransactionCalls.first; + final span = call.transaction.spans.first; + + expect(span.data['file.async'], async); + expect(span.data['file.path'], null); + } + + test('does not add file path if sendDefaultPii is disabled async', + () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + await sut.readAsBytes(); + + await tr.finish(); + + _assertSpan(true); + }); + + test('does not add file path if sendDefaultPii is disabled sync', () async { + final file = File('test_resources/testfile.txt'); + + final sut = fixture.getSut( + file, + tracesSampleRate: 1.0, + ); + + final tr = fixture.hub.startTransaction('name', 'op', bindToScope: true); + + sut.readAsBytesSync(); + + await tr.finish(); + + _assertSpan(false); + }); + + test('add SentryFileTracing integration', () async { + final file = File('test_resources/testfile.txt'); + + fixture.getSut( + file, + tracesSampleRate: 1.0, + ); + + expect(fixture.hub.options.sdk.integrations.contains('SentryFileTracing'), + true); + }); + }); +} + +class Fixture { + final client = MockSentryClient(); + final options = SentryOptions(dsn: fakeDsn); + late Hub hub; + + SentryFile getSut( + File file, { + bool sendDefaultPii = false, + double? tracesSampleRate, + }) { + options.sendDefaultPii = sendDefaultPii; + options.tracesSampleRate = tracesSampleRate; + + hub = Hub(options); + hub.bindClient(client); + return SentryFile(file, hub: hub); + } +} diff --git a/file/test/version_test.dart b/file/test/version_test.dart new file mode 100644 index 000000000..57d12e357 --- /dev/null +++ b/file/test/version_test.dart @@ -0,0 +1,19 @@ +// ignore_for_file: depend_on_referenced_packages + +@TestOn('vm') + +import 'dart:io'; + +import 'package:sentry/src/version.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart' as yaml; + +void main() { + group('sdkVersion', () { + test('matches that of pubspec.yaml', () { + final dynamic pubspec = + yaml.loadYaml(File('pubspec.yaml').readAsStringSync()); + expect(sdkVersion, pubspec['version']); + }); + }); +} diff --git a/file/test_resources/sentry.png b/file/test_resources/sentry.png new file mode 100644 index 0000000000000000000000000000000000000000..2225be472dcbe274d67c07a4995ac46fbe620ff5 GIT binary patch literal 3535 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 JPDHLkV1hwO&o}@8 literal 0 HcmV?d00001 diff --git a/file/test_resources/testfile.txt b/file/test_resources/testfile.txt new file mode 100644 index 000000000..96c906756 --- /dev/null +++ b/file/test_resources/testfile.txt @@ -0,0 +1 @@ +foo bar \ No newline at end of file diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 6d9d3a2fa..2f899397f 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: dio: ^4.0.0 logging: ^1.0.0 package_info_plus: ^3.0.0 + path_provider: ^2.0.0 dev_dependencies: flutter_lints: ^2.0.0 diff --git a/min_version_test/lib/main.dart b/min_version_test/lib/main.dart index f4b277b18..e70e318bf 100644 --- a/min_version_test/lib/main.dart +++ b/min_version_test/lib/main.dart @@ -1,25 +1,52 @@ import 'package:flutter/material.dart'; +import 'dart:io' if (dart.library.html) 'dart:html'; + import 'package:logging/logging.dart'; import 'package:dio/dio.dart'; +import 'package:sentry/sentry.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_dio/sentry_dio.dart'; import 'package:sentry_logging/sentry_logging.dart'; +import 'package:sentry_file/sentry_file.dart'; // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String _exampleDsn = 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; Future main() async { - await SentryFlutter.init( - (options) { - options.dsn = _exampleDsn; - options.addIntegration(LoggingIntegration()); - }, - // Init your App. - appRunner: () => runApp(const MyApp()), - ); + await setupSentry(() => runApp( + SentryScreenshotWidget( + child: SentryUserInteractionWidget( + child: DefaultAssetBundle( + bundle: SentryAssetBundle(enableStructuredDataTracing: true), + child: const MyApp(), + ), + ), + ), + )); +} + +Future setupSentry(AppRunner appRunner) async { + await SentryFlutter.init((options) { + options.dsn = _exampleDsn; + options.tracesSampleRate = 1.0; + options.attachThreads = true; + options.enableWindowMetricBreadcrumbs = true; + options.addIntegration(LoggingIntegration()); + options.sendDefaultPii = true; + options.reportSilentFlutterErrors = true; + options.enableNdkScopeSync = true; + options.enableUserInteractionTracing = true; + options.attachScreenshot = true; + // We can enable Sentry debug logging during development. This is likely + // going to log too much for your app, but can be useful when figuring out + // configuration issues, e.g. finding out why your events are not uploaded. + options.debug = true; + }, + // Init your App. + appRunner: appRunner); } class MyApp extends StatelessWidget { @@ -70,6 +97,12 @@ class _MyHomePageState extends State { Future _incrementCounter() async { setState(() async { + final transaction = Sentry.startTransaction( + 'incrementCounter', + 'task', + bindToScope: true, + ); + // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed @@ -78,12 +111,19 @@ class _MyHomePageState extends State { _counter++; final dio = Dio(); - dio.addSentry(); + dio.addSentry(captureFailedRequests: true); final log = Logger('_MyHomePageState'); + try { - await dio.get('https://flutter.dev/'); + final file = File('response.txt'); + final sentryFile = file.sentryTrace(); + final response = await dio.get('https://flutter.dev/'); + await sentryFile.writeAsString(response.data ?? 'no response'); + + await transaction.finish(status: SpanStatus.ok()); } catch (exception, stackTrace) { log.info(exception.toString(), exception, stackTrace); + await transaction.finish(status: SpanStatus.internalError()); } }); } diff --git a/min_version_test/pubspec.yaml b/min_version_test/pubspec.yaml index 368deb46d..4ecca9a59 100644 --- a/min_version_test/pubspec.yaml +++ b/min_version_test/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: sentry_flutter: sentry_dio: sentry_logging: + sentry_file: dio: ^4.0.0 logging: ^1.0.0 @@ -52,6 +53,8 @@ dependency_overrides: path: ../dio sentry_logging: path: ../logging + sentry_file: + path: ../file # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 690701561..e8c23baa7 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}; do +for pkg in {dart,flutter,logging,dio,file}; do # Bump version in pubspec.yaml perl -pi -e "s/^version: .*/version: $NEW_VERSION/" $pkg/pubspec.yaml # Bump sentry dependency version in pubspec.yaml