diff --git a/CHANGELOG.md b/CHANGELOG.md index 35f0b5c9e2..43b148d0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,62 @@ - [diff](https://github.com/getsentry/sentry-java/compare/7.20.1...8.1.0) ## Unreleased +## 8.13.0 + +### Breaking changes + +- Remove Metrics API ([#2571](https://github.com/getsentry/sentry-dart/pull/2571)) + - The Metrics product never reached maturity from beta and has officially ended in October 7th, 2024 + - Read [this post](https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Metrics-Beta-Ended-on-October-7th) for more information + +### Features + +- Add `beforeCapture` for View Hierarchy ([#2523](https://github.com/getsentry/sentry-dart/pull/2523)) + - View hierarchy calls are now debounced for 2 seconds. +- JS SDK integration ([#2572](https://github.com/getsentry/sentry-dart/pull/2572)) + - Enable the integration by setting `options.enableSentryJs = true` + - Features: + - Sending envelopes through Sentry JS transport layer + - Capturing native JS errors +- Add SentryReplayQuality setting (`options.experimental.replay.quality`) ([#2582](https://github.com/getsentry/sentry-dart/pull/2582)) +- SPM Support ([#2280](https://github.com/getsentry/sentry-dart/pull/2280)) + +### Enhancements + +- Replay: improve iOS native interop performance ([#2530](https://github.com/getsentry/sentry-dart/pull/2530), [#2573](https://github.com/getsentry/sentry-dart/pull/2573)) +- Replay: improve orientation change tracking accuracy on Android ([#2540](https://github.com/getsentry/sentry-dart/pull/2540)) +- Print a warning if the rate limit was reached ([#2595](https://github.com/getsentry/sentry-dart/pull/2595)) +- Add replay masking config to tags and report SDKs versions ([#2592](https://github.com/getsentry/sentry-dart/pull/2592)) +- Enable `options.debug` when in debug mode ([#2597](https://github.com/getsentry/sentry-dart/pull/2597)) +- Propagate sample seed in baggage header ([#2629](https://github.com/getsentry/sentry-dart/pull/2629)) + - Read more about the specs [here](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) +- Finish and start new transaction when tapping same element again ([#2623](https://github.com/getsentry/sentry-dart/pull/2623)) ### Fixes +- Replay: fix masking for frames captured during UI changes ([#2553](https://github.com/getsentry/sentry-dart/pull/2553), [#2657](https://github.com/getsentry/sentry-dart/pull/2657)) +- Replay: fix widget masks overlap when navigating between screens ([#2486](https://github.com/getsentry/sentry-dart/pull/2486), [#2576](https://github.com/getsentry/sentry-dart/pull/2576)) +- WASM compat for Drift ([#2580](https://github.com/getsentry/sentry-dart/pull/2580)) +- Fix image flickering when using `SentryAssetBundle` ([#2577](https://github.com/getsentry/sentry-dart/pull/2577)) - Fix print recursion detection ([#2624](https://github.com/getsentry/sentry-dart/pull/2624)) + +### Misc + +- Transfer ownership of `sentry_link` to Sentry. You can view the changelog for the previous versions [here](https://github.com/getsentry/sentry-dart/blob/main/link/CHANGELOG_OLD.md) ([#2338](https://github.com/getsentry/sentry-dart/pull/2338)) + - No functional changes have been made. This version is identical to the previous one. + - Change license from Apache to MIT + ### Dependencies -- Bump Cocoa SDK from v8.44.0-beta.1 to v8.44.0 ([#2649](https://github.com/getsentry/sentry-dart/pull/2649)) +- Bump Native SDK from v0.7.17 to v0.7.19 ([#2578](https://github.com/getsentry/sentry-dart/pull/2578), [#2588](https://github.com/getsentry/sentry-dart/pull/2588)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0719) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.17...0.7.19) +- Bump Android SDK from v7.19.0 to v7.20.1 ([#2536](https://github.com/getsentry/sentry-dart/pull/2536), [#2549](https://github.com/getsentry/sentry-dart/pull/2549), [#2593](https://github.com/getsentry/sentry-dart/pull/2593)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7201) + - [diff](https://github.com/getsentry/sentry-java/compare/7.20.0...7.20.1) +- Bump Cocoa SDK from v8.42.0 to v8.44.0 ([#2542](https://github.com/getsentry/sentry-dart/pull/2542), [#2548](https://github.com/getsentry/sentry-dart/pull/2548), [#2598](https://github.com/getsentry/sentry-dart/pull/2598), [#2649](https://github.com/getsentry/sentry-dart/pull/2649)) - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8440) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.44.0-beta.1...8.44.0) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.42.0...8.44.0) ## 8.13.0-beta.3 diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index a8b38e375f..8def45ca0f 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -1,11 +1,12 @@ import 'package:meta/meta.dart'; -import 'scope.dart'; import 'protocol.dart'; - +import 'scope.dart'; import 'sentry_options.dart'; class SentryBaggage { static const String _sampleRateKeyName = 'sentry-sample_rate'; + static const String _sampleRandKeyName = 'sentry-sample_rand'; + static const int _maxChars = 8192; static const int _maxListMember = 64; @@ -194,6 +195,10 @@ class SentryBaggage { set(_sampleRateKeyName, value); } + void setSampleRand(String value) { + set(_sampleRandKeyName, value); + } + void setSampled(String value) { set('sentry-sampled', value); } @@ -207,6 +212,15 @@ class SentryBaggage { return double.tryParse(sampleRate); } + double? getSampleRand() { + final sampleRand = get(_sampleRandKeyName); + if (sampleRand == null) { + return null; + } + + return double.tryParse(sampleRand); + } + void setReplayId(String value) => set('sentry-replay_id', value); SentryId? getReplayId() { diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index e17b2b91f8..c80aae9fea 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -1,7 +1,7 @@ import 'package:meta/meta.dart'; -import 'protocol/sentry_id.dart'; import 'protocol/access_aware_map.dart'; +import 'protocol/sentry_id.dart'; import 'sentry_baggage.dart'; import 'sentry_options.dart'; @@ -15,6 +15,7 @@ class SentryTraceContextHeader { this.userSegment, this.transaction, this.sampleRate, + this.sampleRand, this.sampled, this.unknown, this.replayId, @@ -30,6 +31,7 @@ class SentryTraceContextHeader { final String? userSegment; final String? transaction; final String? sampleRate; + final String? sampleRand; final String? sampled; @internal @@ -102,6 +104,9 @@ class SentryTraceContextHeader { if (sampleRate != null) { baggage.setSampleRate(sampleRate!); } + if (sampleRand != null) { + baggage.setSampleRand(sampleRand!); + } if (sampled != null) { baggage.setSampled(sampled!); } @@ -113,6 +118,7 @@ class SentryTraceContextHeader { factory SentryTraceContextHeader.fromBaggage(SentryBaggage baggage) { return SentryTraceContextHeader( + // TODO: implement and use proper get methods here SentryId.fromId(baggage.get('sentry-trace_id').toString()), baggage.get('sentry-public_key').toString(), release: baggage.get('sentry-release'), diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 05a1fd87d0..9dc6cb929f 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -385,6 +385,7 @@ class SentryTracer extends ISentrySpan { transaction: _isHighQualityTransactionName(transactionNameSource) ? name : null, sampleRate: _sampleRateToString(_rootSpan.samplingDecision?.sampleRate), + sampleRand: _sampleRandToString(_rootSpan.samplingDecision?.sampleRand), sampled: _rootSpan.samplingDecision?.sampled.toString(), ); @@ -398,6 +399,13 @@ class SentryTracer extends ISentrySpan { return sampleRate != null ? SampleRateFormat().format(sampleRate) : null; } + String? _sampleRandToString(double? sampleRand) { + if (!isValidSampleRand(sampleRand)) { + return null; + } + return sampleRand != null ? SampleRateFormat().format(sampleRand) : null; + } + bool _isHighQualityTransactionName(SentryTransactionNameSource source) { return source != SentryTransactionNameSource.url; } diff --git a/dart/lib/src/sentry_traces_sampler.dart b/dart/lib/src/sentry_traces_sampler.dart index b842514481..f668ad460f 100644 --- a/dart/lib/src/sentry_traces_sampler.dart +++ b/dart/lib/src/sentry_traces_sampler.dart @@ -31,12 +31,9 @@ class SentryTracesSampler { final tracesSampler = _options.tracesSampler; if (tracesSampler != null) { try { - final result = tracesSampler(samplingContext); - if (result != null) { - return SentryTracesSamplingDecision( - _sample(result), - sampleRate: result, - ); + final sampleRate = tracesSampler(samplingContext); + if (sampleRate != null) { + return _makeSampleDecision(sampleRate); } } catch (exception, stackTrace) { _options.logger( @@ -64,10 +61,7 @@ class SentryTracesSampler { double? optionsOrDefaultRate = optionsRate ?? defaultRate; if (optionsOrDefaultRate != null) { - return SentryTracesSamplingDecision( - _sample(optionsOrDefaultRate), - sampleRate: optionsOrDefaultRate, - ); + return _makeSampleDecision(optionsOrDefaultRate); } return SentryTracesSamplingDecision(false); @@ -78,8 +72,18 @@ class SentryTracesSampler { if (optionsRate == null || !tracesSamplingDecision.sampled) { return false; } - return _sample(optionsRate); + return _isSampled(optionsRate); } - bool _sample(double result) => !(result < _random.nextDouble()); + SentryTracesSamplingDecision _makeSampleDecision(double sampleRate) { + final sampleRand = _random.nextDouble(); + final sampled = _isSampled(sampleRate, sampleRand: sampleRand); + return SentryTracesSamplingDecision(sampled, + sampleRate: sampleRate, sampleRand: sampleRand); + } + + bool _isSampled(double sampleRate, {double? sampleRand}) { + final rand = sampleRand ?? _random.nextDouble(); + return rand <= sampleRate; + } } diff --git a/dart/lib/src/sentry_traces_sampling_decision.dart b/dart/lib/src/sentry_traces_sampling_decision.dart index 90161515cf..802d27f832 100644 --- a/dart/lib/src/sentry_traces_sampling_decision.dart +++ b/dart/lib/src/sentry_traces_sampling_decision.dart @@ -2,8 +2,10 @@ class SentryTracesSamplingDecision { SentryTracesSamplingDecision( this.sampled, { this.sampleRate, + this.sampleRand, }); final bool sampled; final double? sampleRate; + final double? sampleRand; } diff --git a/dart/lib/src/sentry_transaction_context.dart b/dart/lib/src/sentry_transaction_context.dart index 32ab0324b7..9caa170385 100644 --- a/dart/lib/src/sentry_transaction_context.dart +++ b/dart/lib/src/sentry_transaction_context.dart @@ -1,27 +1,27 @@ import 'package:meta/meta.dart'; -import 'sentry_trace_origins.dart'; import 'protocol.dart'; import 'sentry_baggage.dart'; +import 'sentry_trace_origins.dart'; import 'tracing.dart'; @immutable class SentryTransactionContext extends SentrySpanContext { final String name; - final SentryTracesSamplingDecision? parentSamplingDecision; final SentryTransactionNameSource? transactionNameSource; final SentryTracesSamplingDecision? samplingDecision; + final SentryTracesSamplingDecision? parentSamplingDecision; SentryTransactionContext( this.name, String operation, { super.description, - this.parentSamplingDecision, super.traceId, super.spanId, super.parentSpanId, this.transactionNameSource, this.samplingDecision, + this.parentSamplingDecision, super.origin, }) : super( operation: operation, @@ -35,6 +35,7 @@ class SentryTransactionContext extends SentrySpanContext { SentryBaggage? baggage, }) { final sampleRate = baggage?.getSampleRate(); + final sampleRand = baggage?.getSampleRand(); return SentryTransactionContext( name, operation, @@ -44,6 +45,7 @@ class SentryTransactionContext extends SentrySpanContext { ? SentryTracesSamplingDecision( traceHeader.sampled!, sampleRate: sampleRate, + sampleRand: sampleRand, ) : null, transactionNameSource: diff --git a/dart/lib/src/utils/tracing_utils.dart b/dart/lib/src/utils/tracing_utils.dart index 6198062ddc..5a14311d1c 100644 --- a/dart/lib/src/utils/tracing_utils.dart +++ b/dart/lib/src/utils/tracing_utils.dart @@ -80,3 +80,10 @@ bool isValidSampleRate(double? sampleRate) { } return !sampleRate.isNaN && sampleRate >= 0.0 && sampleRate <= 1.0; } + +bool isValidSampleRand(double? sampleRand) { + if (sampleRand == null) { + return false; + } + return !sampleRand.isNaN && sampleRand >= 0.0 && sampleRand < 1.0; +} diff --git a/dart/lib/src/version.dart b/dart/lib/src/version.dart index 3cfe70c430..19b57a05e5 100644 --- a/dart/lib/src/version.dart +++ b/dart/lib/src/version.dart @@ -9,7 +9,7 @@ library; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; String sdkName(bool isWeb) => isWeb ? _browserSdkName : _ioSdkName; diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index d55e506d8d..562de17c7e 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 8.13.0-beta.3 +version: 8.13.0 description: > A crash reporting library for Dart that sends crash reports to Sentry.io. This library supports Dart VM and Web. For Flutter consider sentry_flutter instead. diff --git a/dart/test/protocol/sentry_baggage_header_test.dart b/dart/test/protocol/sentry_baggage_header_test.dart index 910929776e..6d75d6dd15 100644 --- a/dart/test/protocol/sentry_baggage_header_test.dart +++ b/dart/test/protocol/sentry_baggage_header_test.dart @@ -21,6 +21,7 @@ void main() { baggage.setUserSegment('userSegment'); baggage.setTransaction('transaction'); baggage.setSampleRate('1.0'); + baggage.setSampleRand('0.4'); baggage.setSampled('false'); final replayId = SentryId.newId().toString(); baggage.setReplayId(replayId); @@ -37,6 +38,7 @@ void main() { 'sentry-user_segment=userSegment,' 'sentry-transaction=transaction,' 'sentry-sample_rate=1.0,' + 'sentry-sample_rand=0.4,' 'sentry-sampled=false,' 'sentry-replay_id=$replayId'); }); diff --git a/dart/test/sentry_tracer_test.dart b/dart/test/sentry_tracer_test.dart index 667e43b159..d5dde5ae98 100644 --- a/dart/test/sentry_tracer_test.dart +++ b/dart/test/sentry_tracer_test.dart @@ -487,6 +487,7 @@ void main() { SentryTracesSamplingDecision( true, sampleRate: 1.0, + sampleRand: 0.8, ); final _context = SentryTransactionContext( 'name', @@ -512,6 +513,7 @@ void main() { expect(newBaggage.get('sentry-user_segment'), 'segment'); expect(newBaggage.get('sentry-transaction'), 'name'); expect(newBaggage.get('sentry-sample_rate'), '1'); + expect(newBaggage.getSampleRand(), 0.8); expect(newBaggage.get('sentry-sampled'), 'true'); }); diff --git a/dio/lib/src/version.dart b/dio/lib/src/version.dart index 98da0c3059..f356d79579 100644 --- a/dio/lib/src/version.dart +++ b/dio/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.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 9df475f995..fa41e6733d 100644 --- a/dio/pubspec.yaml +++ b/dio/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_dio description: An integration which adds support for performance tracing for the Dio package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -19,7 +19,7 @@ platforms: dependencies: dio: ^5.0.0 - sentry: 8.13.0-beta.3 + sentry: 8.13.0 dev_dependencies: meta: ^1.3.0 diff --git a/drift/lib/src/version.dart b/drift/lib/src/version.dart index ea25bb0ba6..5386931faf 100644 --- a/drift/lib/src/version.dart +++ b/drift/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_drift'; diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index e6dc4f6f94..2d0c84ab80 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_drift description: An integration which adds support for performance tracing for the drift package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: web: dependencies: - sentry: 8.13.0-beta.3 + sentry: 8.13.0 meta: ^1.3.0 drift: ^2.13.0 diff --git a/file/lib/src/version.dart b/file/lib/src/version.dart index ca08a53d5e..549212a6cb 100644 --- a/file/lib/src/version.dart +++ b/file/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.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 bfc9a4aa9b..4069800ff9 100644 --- a/file/pubspec.yaml +++ b/file/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_file description: An integration which adds support for performance tracing for dart.io.File. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: windows: dependencies: - sentry: 8.13.0-beta.3 + sentry: 8.13.0 meta: ^1.3.0 dev_dependencies: diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 3c40bb5583..c1ecbf8e19 100644 --- a/flutter/example/pubspec.yaml +++ b/flutter/example/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_flutter_example description: Demonstrates how to use the sentry_flutter plugin. -version: 8.13.0-beta.3 +version: 8.13.0 publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 4d2446c2a4..23ee438948 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -9,7 +9,6 @@ import '../screenshot/recorder.dart'; import '../screenshot/recorder_config.dart'; import 'package:flutter/widgets.dart' as widget; -import '../screenshot/stabilizer.dart'; import '../utils/debouncer.dart'; class ScreenshotEventProcessor implements EventProcessor { @@ -126,37 +125,6 @@ class ScreenshotEventProcessor implements EventProcessor { } @internal - Future createScreenshot() async { - if (_options.experimental.privacyForScreenshots == null) { - return _recorder.capture((screenshot) => - screenshot.pngData.then((v) => v.buffer.asUint8List())); - } else { - // If masking is enabled, we need to use [ScreenshotStabilizer]. - final completer = Completer(); - final stabilizer = ScreenshotStabilizer( - _recorder, _options, - (screenshot) async { - final pngData = await screenshot.pngData; - completer.complete(pngData.buffer.asUint8List()); - }, - // This limits the amount of time to take a stable masked screenshot. - maxTries: 5, - // We need to force the frame the frame or this could hang indefinitely. - frameSchedulingMode: FrameSchedulingMode.forced, - ); - try { - unawaited( - stabilizer.capture(Duration.zero).onError(completer.completeError)); - // DO NOT return completer.future directly - we need to dispose first. - return await completer.future.timeout(const Duration(seconds: 1), - onTimeout: () { - _options.logger( - SentryLevel.warning, 'Timed out taking a stable screenshot.'); - return null; - }); - } finally { - stabilizer.dispose(); - } - } - } + Future createScreenshot() => _recorder.capture( + (screenshot) => screenshot.pngData.then((v) => v.buffer.asUint8List())); } diff --git a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart index a39a3cc6de..6393b2656e 100644 --- a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart +++ b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart @@ -6,24 +6,24 @@ import '../../../sentry_flutter.dart'; import '../../replay/replay_recorder.dart'; import '../../screenshot/recorder.dart'; import '../../screenshot/recorder_config.dart'; -import '../../screenshot/stabilizer.dart'; import '../native_memory.dart'; @internal class CocoaReplayRecorder { final SentryFlutterOptions _options; final ScreenshotRecorder _recorder; - late final ScreenshotStabilizer _stabilizer; - var _completer = Completer?>(); CocoaReplayRecorder(this._options) : _recorder = ReplayScreenshotRecorder( - ScreenshotRecorderConfig( - pixelRatio: - _options.experimental.replay.quality.resolutionScalingFactor, - ), - _options) { - _stabilizer = ScreenshotStabilizer(_recorder, _options, (screenshot) async { + ScreenshotRecorderConfig( + pixelRatio: + _options.experimental.replay.quality.resolutionScalingFactor, + ), + _options, + ); + + Future?> captureScreenshot() async { + return _recorder.capture((screenshot) async { final data = await screenshot.rawRgbaData; _options.logger( SentryLevel.debug, @@ -35,15 +35,7 @@ class CocoaReplayRecorder { final json = data.toNativeMemory().toJson(); json['width'] = screenshot.width; json['height'] = screenshot.height; - _completer.complete(json); - }); - } - - Future?> captureScreenshot() async { - _completer = Completer(); - _stabilizer.ensureFrameAndAddCallback((msSinceEpoch) { - _stabilizer.capture(msSinceEpoch).onError(_completer.completeError); + return json; }); - return _completer.future; } } diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index 06b75a2a60..4a79dd832d 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'package:flutter/scheduler.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; -import '../screenshot/stabilizer.dart'; import '../screenshot/screenshot.dart'; import 'replay_recorder.dart'; import 'scheduled_recorder_config.dart'; @@ -19,7 +19,6 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { late final ScheduledScreenshotRecorderCallback _callback; var _status = _Status.running; late final Duration _frameDuration; - late final ScreenshotStabilizer _stabilizer; // late final _idleFrameFiller = _IdleFrameFiller(_frameDuration, _onScreenshot); @override @@ -35,15 +34,23 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { _frameDuration = Duration(milliseconds: 1000 ~/ config.frameRate); assert(_frameDuration.inMicroseconds > 0); - _stabilizer = ScreenshotStabilizer(this, options, _onImageCaptured); - _scheduler = Scheduler(_frameDuration, _stabilizer.capture, - _stabilizer.ensureFrameAndAddCallback); + _scheduler = Scheduler( + _frameDuration, + (_) => capture(_onImageCaptured), + _addPostFrameCallback, + ); if (callback != null) { _callback = callback; } } + void _addPostFrameCallback(FrameCallback callback) { + options.bindingUtils.instance! + ..ensureVisualUpdate() + ..addPostFrameCallback(callback); + } + set callback(ScheduledScreenshotRecorderCallback callback) { _callback = callback; } @@ -63,12 +70,10 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { } Future _stopScheduler() { - _stabilizer.stopped = true; return _scheduler.stop(); } void _startScheduler() { - _stabilizer.stopped = false; _scheduler.start(); // We need to schedule a frame because if this happens in-between user @@ -82,7 +87,6 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { options.logger(SentryLevel.debug, "$logName: stopping capture."); _status = _Status.stopped; await _stopScheduler(); - _stabilizer.dispose(); // await Future.wait([_stopScheduler(), _idleFrameFiller.stop()]); options.logger(SentryLevel.debug, "$logName: capture stopped."); } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 3869e4e1b3..acd4f9ca49 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -190,6 +190,18 @@ class _Capture { final recorder = PictureRecorder(); final canvas = Canvas(recorder); final image = await futureImage; + + // Note: there's a weird bug when we write image to canvas directly. + // If the UI is updating quickly in some apps, the image could get + // out-of-sync with the UI and/or it can get completely mangled. + // This can be reproduced, for example, by switching between Spotube's + // Search vs Library (2nd and 3rd bottom bar buttons). + // Weirdly, dumping the image data seems to prevent this issue... + { + // we do so in a block so it can be GC'ed early. + final _ = await image.toByteData(); + } + try { canvas.drawImage(image, Offset.zero, Paint()); } finally { diff --git a/flutter/lib/src/screenshot/stabilizer.dart b/flutter/lib/src/screenshot/stabilizer.dart deleted file mode 100644 index 72ee1df15a..0000000000 --- a/flutter/lib/src/screenshot/stabilizer.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/scheduler.dart'; -import 'package:meta/meta.dart'; - -import '../../sentry_flutter.dart'; -import 'recorder.dart'; -import 'screenshot.dart'; - -/// We're facing an issue: the tree walked with visitChildElements() is out of -/// sync to what is currently rendered by RenderRepaintBoundary.toImage(), -/// even though there's no async gap between these two. This causes masks to -/// be off during repaints, e.g. when scrolling a view or when text is rendered -/// in different places between two screens. This is most easily reproducible -/// when there's no animation between the two screens. -/// For example, Spotube's Search vs Library (2nd and 3rd bottom bar buttons). -/// -/// To get around this issue, we're taking two subsequent screenshots -/// (after two frames) and only actually capture a screenshot if the -/// two are exactly the same. -@internal -class ScreenshotStabilizer { - final SentryFlutterOptions _options; - final ScreenshotRecorder _recorder; - final Future Function(Screenshot screenshot) _callback; - final int? maxTries; - final FrameSchedulingMode frameSchedulingMode; - Screenshot? _previousScreenshot; - int _tries = 0; - bool stopped = false; - - ScreenshotStabilizer(this._recorder, this._options, this._callback, - {this.maxTries, this.frameSchedulingMode = FrameSchedulingMode.normal}) { - assert(maxTries == null || maxTries! > 1, - "Cannot use ScreenshotStabilizer if we cannot retry at least once."); - } - - void dispose() { - _previousScreenshot?.dispose(); - _previousScreenshot = null; - } - - void ensureFrameAndAddCallback(FrameCallback callback) { - final binding = _options.bindingUtils.instance!; - switch (frameSchedulingMode) { - case FrameSchedulingMode.normal: - binding.scheduleFrame(); - break; - case FrameSchedulingMode.forced: - binding.scheduleForcedFrame(); - break; - } - binding.addPostFrameCallback(callback); - } - - Future capture(Duration _) { - _tries++; - return _recorder.capture(_onImageCaptured); - } - - Future _onImageCaptured(Screenshot screenshot) async { - if (stopped) { - _tries = 0; - return; - } - - var prevScreenshot = _previousScreenshot; - try { - _previousScreenshot = screenshot.clone(); - if (prevScreenshot != null && - await prevScreenshot.hasSameImageAs(screenshot)) { - // Sucessfully captured a stable screenshot (repeated at least twice). - _tries = 0; - - // If it's from the same (retry) flow, use the first screenshot - // timestamp. Otherwise this was called from a scheduler (in a new flow) - // so use the new timestamp. - await _callback((prevScreenshot.flow.id == screenshot.flow.id) - ? prevScreenshot - : screenshot); - - // Do not just return the Future resulting from callback(). - // We need to await here so that the dispose runs ASAP. - return; - } - } finally { - // [prevScreenshot] and [screenshot] are unnecessary after this line. - // Note: we need to dispose (free the memory) before recursion. - // Also, we need to reset the variable to null so that the whole object - // can be garbage collected. - prevScreenshot?.dispose(); - prevScreenshot = null; - // Note: while the caller will also do `screenshot.dispose()`, - // it would be a problem in a long recursion because we only return - // from this function when the screenshot is ultimately stable. - // At that point, the caller would have accumulated a lot of screenshots - // on stack. This would lead to OOM. - screenshot.dispose(); - } - - if (maxTries != null && _tries >= maxTries!) { - throw Exception('Failed to capture a stable screenshot. ' - 'Giving up after $_tries tries.'); - } else { - // Add a delay to give the UI a chance to stabilize. - // Only do this on every other frame so that there's a greater chance - // of two subsequent frames being the same. - final sleepMs = _tries % 2 == 1 ? min(100, 10 * (_tries - 1)) : 0; - - if (_tries > 1) { - _options.logger( - SentryLevel.debug, - '${_recorder.logName}: ' - 'Retrying screenshot capture due to UI changes. ' - 'Delay before next capture: $sleepMs ms.'); - } - - if (sleepMs > 0) { - await Future.delayed(Duration(milliseconds: sleepMs)); - } - - final completer = Completer(); - ensureFrameAndAddCallback((Duration sinceSchedulerEpoch) async { - _tries++; - try { - await _recorder.capture(_onImageCaptured, screenshot.flow); - completer.complete(); - } catch (e, stackTrace) { - completer.completeError(e, stackTrace); - } - }); - return completer.future; - } - } -} - -@internal -enum FrameSchedulingMode { - /// The frame is scheduled only if the UI is visible. - /// If you await for the callback, it may take indefinitely long if the - /// app is in the background. - normal, - - /// A forced frame is scheduled immediately regardless of the UI visibility. - forced, -} diff --git a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart index 4432597e3d..a9fc134fcf 100644 --- a/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart +++ b/flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart @@ -210,6 +210,9 @@ import '../../sentry_flutter.dart'; import '../widget_utils.dart'; import 'user_interaction_info.dart'; +// ignore: implementation_imports +import 'package:sentry/src/sentry_tracer.dart'; + const _tapDeltaArea = 20 * 20; Element? _clickTrackerElement; @@ -428,18 +431,22 @@ class _SentryUserInteractionWidgetState lastElement?.widget == element.widget && !activeTransaction.finished) { // ignore: invalid_use_of_internal_member - activeTransaction.scheduleFinish(); - return; + if (activeTransaction is SentryTracer && + activeTransaction.children.isNotEmpty) { + activeTransaction.finish(); + } else { + activeTransaction.finish(status: SpanStatus.cancelled()); + } } else { activeTransaction.finish(); - _hub.configureScope((scope) { - if (scope.span == activeTransaction) { - scope.span = null; - } - }); - _activeTransaction = null; - _lastTappedWidget = null; } + _hub.configureScope((scope) { + if (scope.span == activeTransaction) { + scope.span = null; + } + }); + _activeTransaction = null; + _lastTappedWidget = null; } _lastTappedWidget = info; diff --git a/flutter/lib/src/version.dart b/flutter/lib/src/version.dart index 74155163d3..818e02f7ba 100644 --- a/flutter/lib/src/version.dart +++ b/flutter/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; /// The default SDK name reported to Sentry.io in the submitted events. const String sdkName = 'sentry.dart.flutter'; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 121c289ad2..d74dd4da63 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry_flutter -version: 8.13.0-beta.3 +version: 8.13.0 description: Sentry SDK for Flutter. This package aims to support different Flutter targets by relying on the many platforms supported by Sentry with native SDKs. homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart @@ -23,7 +23,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - sentry: 8.13.0-beta.3 + sentry: 8.13.0 package_info_plus: '>=1.0.0' meta: ^1.3.0 ffi: ^2.0.0 diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index c433628f24..f74a3fe708 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -68,27 +68,6 @@ void main() { await _addScreenshotAttachment(tester, null, added: true, isWeb: false); }); - testWidgets('does not block if the screenshot fails to stabilize', - (tester) async { - fixture.options.automatedTestMode = false; - fixture.options.experimental.privacy.maskAllText = true; - // Run with real async https://stackoverflow.com/a/54021863 - await tester.runAsync(() async { - final sut = fixture.getSut(null, false); - - await tester.pumpWidget(SentryScreenshotWidget( - child: Text('Catching Pokémon is a snap!', - textDirection: TextDirection.ltr))); - - final throwable = Exception(); - event = SentryEvent(throwable: throwable); - hint = Hint(); - await sut.apply(event, hint); - - expect(hint.screenshot, isNull); - }); - }); - testWidgets('adds screenshot attachment with canvasKit renderer', (tester) async { await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index bbbb347d17..515d61a7ff 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -36,6 +36,10 @@ void main() { late Map replayConfig; setUp(() { + hub = MockHub(); + fs = MemoryFileSystem.test(); + native = NativeChannelFixture(); + if (mockPlatform.isIOS) { replayConfig = { 'replayId': '123', @@ -48,14 +52,11 @@ void main() { 'height': 600, 'frameRate': 1000, }; + fs.directory(replayConfig['directory']).createSync(recursive: true); + when(native.handler('addReplayScreenshot', any)) + .thenAnswer((_) => Future.value()); } - hub = MockHub(); - - fs = MemoryFileSystem.test(); - - native = NativeChannelFixture(); - options = defaultTestOptions(MockPlatformChecker(mockPlatform: mockPlatform)) ..fileSystem = fs @@ -99,6 +100,10 @@ void main() { expect(scope.replayId, isNull); await closure(scope); expect(scope.replayId.toString(), replayConfig['replayId']); + + if (mockPlatform.isAndroid) { + await native.invokeFromNative('ReplayRecorder.stop'); + } }); }); @@ -126,8 +131,7 @@ void main() { await pumpTestElement(tester); if (mockPlatform.isAndroid) { - final replayDir = fs.directory(replayConfig['directory']) - ..createSync(recursive: true); + final replayDir = fs.directory(replayConfig['directory']); var callbackFinished = Completer(); diff --git a/flutter/test/user_interaction/sentry_user_interaction_widget_test.dart b/flutter/test/user_interaction/sentry_user_interaction_widget_test.dart index 7d35c3deaa..b77e83242d 100644 --- a/flutter/test/user_interaction/sentry_user_interaction_widget_test.dart +++ b/flutter/test/user_interaction/sentry_user_interaction_widget_test.dart @@ -2,11 +2,10 @@ library; // ignore_for_file: invalid_use_of_internal_member -import 'dart:async'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry/src/sentry_tracer.dart'; @@ -459,7 +458,8 @@ void main() { }); }); - testWidgets('Extend timer if transaction already started for same widget', + testWidgets( + 'Cancel transaction if already started for same widget and start new one', (tester) async { await tester.runAsync(() async { final sut = fixture.getSut( @@ -467,21 +467,50 @@ void main() { enableUserInteractionBreadcrumbs: false); await tapMe(tester, sut, 'btn_1'); - Timer? currentTimer; + SentryTracer? initialTracer; fixture.hub.configureScope((scope) { - final tracer = (scope.span as SentryTracer); - currentTimer = tracer.autoFinishAfterTimer; + initialTracer = (scope.span as SentryTracer); }); - await tapMe(tester, sut, 'btn_1', pumpWidget: false); + await tapMe(tester, sut, 'btn_1'); - Timer? autoFinishAfterTimer; + SentryTracer? tracer; fixture.hub.configureScope((scope) { - final tracer = (scope.span as SentryTracer); - autoFinishAfterTimer = tracer.autoFinishAfterTimer; + tracer = (scope.span as SentryTracer); }); - expect(currentTimer, isNot(equals(autoFinishAfterTimer))); + expect(initialTracer?.finished, isTrue); + expect(initialTracer?.status, equals(SpanStatus.cancelled())); + + expect(initialTracer, isNot(equals(tracer))); + }); + }); + + testWidgets( + 'Finish transaction if already started with children for same widget and start new one', + (tester) async { + await tester.runAsync(() async { + final sut = fixture.getSut( + enableUserInteractionTracing: true, + enableUserInteractionBreadcrumbs: false); + + await tapMe(tester, sut, 'btn_1'); + SentryTracer? initialTracer; + + await fixture.hub.configureScope((scope) async { + initialTracer = (scope.span as SentryTracer); + final child = initialTracer?.startChild("btn_1_child"); + await child?.finish(); + }); + + await tapMe(tester, sut, 'btn_1'); + + SentryTracer? tracer; + fixture.hub.configureScope((scope) { + tracer = (scope.span as SentryTracer); + }); + expect(initialTracer?.finished, isTrue); + expect(initialTracer, isNot(equals(tracer))); }); }); @@ -534,6 +563,9 @@ class Fixture { double? tracesSampleRate = 1.0, bool sendDefaultPii = false, }) { + // Missing mock exception + when(_transport.send(any)).thenAnswer((_) async => SentryId.newId()); + _options.transport = _transport; _options.tracesSampleRate = tracesSampleRate; _options.enableUserInteractionTracing = enableUserInteractionTracing; diff --git a/hive/lib/src/version.dart b/hive/lib/src/version.dart index f4ce52723c..253629ca83 100644 --- a/hive/lib/src/version.dart +++ b/hive/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_hive'; diff --git a/hive/pubspec.yaml b/hive/pubspec.yaml index bcb532f767..18cec07b04 100644 --- a/hive/pubspec.yaml +++ b/hive/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_hive description: An integration which adds support for performance tracing for the hive package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -17,7 +17,7 @@ platforms: web: dependencies: - sentry: 8.13.0-beta.3 + sentry: 8.13.0 hive: ^2.2.3 meta: ^1.3.0 diff --git a/isar/lib/src/version.dart b/isar/lib/src/version.dart index 7b022c04ec..554942d405 100644 --- a/isar/lib/src/version.dart +++ b/isar/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_isar'; diff --git a/isar/pubspec.yaml b/isar/pubspec.yaml index 84203d230d..c3ebe69dc1 100644 --- a/isar/pubspec.yaml +++ b/isar/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_isar description: An integration which adds support for performance tracing for the isar package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -20,7 +20,7 @@ platforms: dependencies: isar: ^3.1.0 isar_flutter_libs: ^3.1.0 # contains Isar Core - sentry: 8.13.0-beta.3 + sentry: 8.13.0 meta: ^1.3.0 path: ^1.8.3 diff --git a/link/pubspec.yaml b/link/pubspec.yaml index 74b5c58143..ee922bce13 100644 --- a/link/pubspec.yaml +++ b/link/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_link description: Automatic capture of exceptions and GraphQL errors for the gql eco-system, like graphql and ferry -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -13,7 +13,7 @@ dependencies: gql_exec: ">=0.4.4 <2.0.0" gql_link: ">=0.5.0 <2.0.0" gql: ">=0.14.0 <2.0.0" - sentry: 8.13.0-beta.3 + sentry: 8.13.0 dev_dependencies: lints: ^4.0.0 diff --git a/logging/lib/src/version.dart b/logging/lib/src/version.dart index 64fa9326dd..b96cae5646 100644 --- a/logging/lib/src/version.dart +++ b/logging/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.0'; /// The package name reported to Sentry.io in the submitted events. const String packageName = 'pub:sentry_logging'; diff --git a/logging/pubspec.yaml b/logging/pubspec.yaml index 7323af9c24..5d6ce53052 100644 --- a/logging/pubspec.yaml +++ b/logging/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_logging description: An integration which adds support for recording log from the logging package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/dart/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -19,7 +19,7 @@ platforms: dependencies: logging: ^1.0.0 - sentry: 8.13.0-beta.3 + sentry: 8.13.0 dev_dependencies: lints: '>=2.0.0' diff --git a/sqflite/lib/src/version.dart b/sqflite/lib/src/version.dart index 4e5c17c573..bac93198ce 100644 --- a/sqflite/lib/src/version.dart +++ b/sqflite/lib/src/version.dart @@ -1,5 +1,5 @@ /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.13.0-beta.3'; +const String sdkVersion = '8.13.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 index 2caa5413af..9daeb00daa 100644 --- a/sqflite/pubspec.yaml +++ b/sqflite/pubspec.yaml @@ -1,6 +1,6 @@ name: sentry_sqflite description: An integration which adds support for performance tracing for the sqflite package. -version: 8.13.0-beta.3 +version: 8.13.0 homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart issue_tracker: https://github.com/getsentry/sentry-dart/issues @@ -15,7 +15,7 @@ platforms: macos: dependencies: - sentry: 8.13.0-beta.3 + sentry: 8.13.0 sqflite: ^2.2.8 sqflite_common: ^2.0.0 meta: ^1.3.0