diff --git a/CHANGELOG.md b/CHANGELOG.md index 93065f1afe..666ed2bbd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,53 @@ ### Features - Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227)) + ```dart await SentryFlutter.init( (options) { - options.dsn = 'https://examplePublicKey@o0.ingest.sentry.io/0'; + ... options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"]; - options.denyUrls = ["^.*ends-with-this\$", "denied-url"]; + options.denyUrls = ["^.*ends-with-this\$", "denied-url"]; + }, + appRunner: () => runApp(MyApp()), + ); + ``` + +- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)). + + To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)): + + ```dart + await SentryFlutter.init( + (options) { ... + options.experimental.replay.sessionSampleRate = 1.0; + options.experimental.replay.errorSampleRate = 1.0; }, appRunner: () => runApp(MyApp()), ); ``` + +### Dependencies + +- Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8360) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.35.1...8.36.0) + +## 8.8.0 + +### Features + +- Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239)) + - This can be used to test if native crash reporting works + - Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218)) - - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. - - Ignored routes will also create no TTID and TTFD spans. -```dart -SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), -``` + - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. + - Ignored routes will also create no TTID and TTFD spans. + + ```dart + SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), + ``` ### Improvements @@ -29,6 +59,29 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), ### Dependencies +- Bump Cocoa SDK from v8.33.0 to v8.35.1 ([#2247](https://github.com/getsentry/sentry-dart/pull/2247)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8351) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.33.0...8.35.1) +- Bump Android SDK from v7.13.0 to v7.14.0 ([#2228](https://github.com/getsentry/sentry-dart/pull/2228)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7140) + - [diff](https://github.com/getsentry/sentry-java/compare/7.13.0...7.14.0) + +## 8.8.0-alpha.1 + +### Features + +- iOS Session Replay Alpha ([#2209](https://github.com/getsentry/sentry-dart/pull/2209)) +- Android replay touch tracking support ([#2228](https://github.com/getsentry/sentry-dart/pull/2228)) +- Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218)) + - This will ignore the Routes and prevent the Route from being pushed to the Sentry server. + - Ignored routes will also create no TTID and TTFD spans. + +```dart +SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), +``` + +### Dependencies + - Bump Android SDK from v7.13.0 to v7.14.0 ([#2228](https://github.com/getsentry/sentry-dart/pull/2228)) - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7140) - [diff](https://github.com/getsentry/sentry-java/compare/7.13.0...7.14.0) @@ -39,6 +92,7 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), - Add support for span level measurements. ([#2214](https://github.com/getsentry/sentry-dart/pull/2214)) - Add `ignoreTransactions` and `ignoreErrors` to options ([#2207](https://github.com/getsentry/sentry-dart/pull/2207)) + ```dart await SentryFlutter.init( (options) { @@ -50,8 +104,10 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), appRunner: () => runApp(MyApp()), ); ``` + - Add proxy support ([#2192](https://github.com/getsentry/sentry-dart/pull/2192)) - Configure a `SentryProxy` object and set it on `SentryFlutter.init` + ```dart import 'package:flutter/widgets.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -91,24 +147,25 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]), - This is enabled automatically and will change grouping if you already have issues with obfuscated titles - If you want to disable this feature, set `enableExceptionTypeIdentification` to `false` in your Sentry options - You can add your custom exception identifier if there are exceptions that we do not identify out of the box -```dart -// How to add your own custom exception identifier -class MyCustomExceptionIdentifier implements ExceptionIdentifier { - @override - String? identifyType(Exception exception) { - if (exception is MyCustomException) { - return 'MyCustomException'; - } - if (exception is MyOtherCustomException) { - return 'MyOtherCustomException'; + + ```dart + // How to add your own custom exception identifier + class MyCustomExceptionIdentifier implements ExceptionIdentifier { + @override + String? identifyType(Exception exception) { + if (exception is MyCustomException) { + return 'MyCustomException'; + } + if (exception is MyOtherCustomException) { + return 'MyOtherCustomException'; + } + return null; } - return null; } -} -SentryFlutter.init((options) => - options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier())); -``` + SentryFlutter.init((options) => + options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier())); + ``` ### Deprecated @@ -124,6 +181,27 @@ SentryFlutter.init((options) => - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7130) - [diff](https://github.com/getsentry/sentry-java/compare/7.12.0...7.13.0) +## 8.6.0-alpha.2 + +### Features + +- Android Session Replay Alpha ([#2032](https://github.com/getsentry/sentry-dart/pull/2032)) + + To try out replay, you can set following options: + + ```dart + await SentryFlutter.init( + (options) { + ... + options.experimental.replay.sessionSampleRate = 1.0; + options.experimental.replay.errorSampleRate = 1.0; + }, + appRunner: () => runApp(MyApp()), + ); + ``` + + Access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/) + ## 8.5.0 ### Features @@ -136,7 +214,7 @@ SentryFlutter.init((options) => ### Fixes - Disable sff & frame delay detection on web, linux and windows ([#2182](https://github.com/getsentry/sentry-dart/pull/2182)) - - Display refresh rate is locked at 60 for these platforms which can lead to inaccurate metrics + - Display refresh rate is locked at 60 for these platforms which can lead to inaccurate metrics ### Improvements diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index 86eabc2ffc..9ece68d032 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -52,6 +52,10 @@ class Breadcrumb { String? httpQuery, String? httpFragment, }) { + // The timestamp is used as the request-end time, so we need to set it right + // now and not rely on the default constructor. + timestamp ??= getUtcDateTime(); + return Breadcrumb( type: 'http', category: 'http', @@ -67,6 +71,11 @@ class Breadcrumb { if (responseBodySize != null) 'response_body_size': responseBodySize, if (httpQuery != null) 'http.query': httpQuery, if (httpFragment != null) 'http.fragment': httpFragment, + if (requestDuration != null) + 'start_timestamp': + timestamp.millisecondsSinceEpoch - requestDuration.inMilliseconds, + if (requestDuration != null) + 'end_timestamp': timestamp.millisecondsSinceEpoch, }, ); } @@ -97,11 +106,32 @@ class Breadcrumb { String? viewClass, }) { final newData = data ?? {}; + var path = ''; + if (viewId != null) { newData['view.id'] = viewId; + path = viewId; + } + + if (newData.containsKey('label')) { + if (path.isEmpty) { + path = newData['label']; + } else { + path = "$path, label: ${newData['label']}"; + } } + if (viewClass != null) { newData['view.class'] = viewClass; + if (path.isEmpty) { + path = viewClass; + } else { + path = "$viewClass($path)"; + } + } + + if (path.isNotEmpty && !newData.containsKey('path')) { + newData['path'] = path; } return Breadcrumb( diff --git a/dart/lib/src/protocol/sentry_trace_context.dart b/dart/lib/src/protocol/sentry_trace_context.dart index 1d80541fd4..e44eede721 100644 --- a/dart/lib/src/protocol/sentry_trace_context.dart +++ b/dart/lib/src/protocol/sentry_trace_context.dart @@ -18,6 +18,9 @@ class SentryTraceContext { /// Id of a parent span final SpanId? parentSpanId; + /// Replay associated with this trace. + final SentryId? replayId; + /// Whether the span is sampled or not final bool? sampled; @@ -50,6 +53,9 @@ class SentryTraceContext { ? null : SpanId.fromId(json['parent_span_id'] as String), traceId: SentryId.fromId(json['trace_id'] as String), + replayId: json['replay_id'] == null + ? null + : SentryId.fromId(json['replay_id'] as String), description: json['description'] as String?, status: json['status'] == null ? null @@ -68,6 +74,7 @@ class SentryTraceContext { 'trace_id': traceId.toString(), 'op': operation, if (parentSpanId != null) 'parent_span_id': parentSpanId!.toString(), + if (replayId != null) 'replay_id': replayId!.toString(), if (description != null) 'description': description, if (status != null) 'status': status!.toString(), if (origin != null) 'origin': origin, @@ -84,6 +91,7 @@ class SentryTraceContext { sampled: sampled, origin: origin, unknown: unknown, + replayId: replayId, ); SentryTraceContext({ @@ -96,6 +104,7 @@ class SentryTraceContext { this.status, this.origin, this.unknown, + this.replayId, }) : traceId = traceId ?? SentryId.newId(), spanId = spanId ?? SpanId.newId(); @@ -103,9 +112,9 @@ class SentryTraceContext { factory SentryTraceContext.fromPropagationContext( PropagationContext propagationContext) { return SentryTraceContext( - traceId: propagationContext.traceId, - spanId: propagationContext.spanId, - operation: 'default', - ); + traceId: propagationContext.traceId, + spanId: propagationContext.spanId, + operation: 'default', + replayId: propagationContext.baggage?.getReplayId()); } } diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 15ce065752..03748445e6 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -97,6 +97,13 @@ class Scope { /// they must be JSON-serializable. Map get extra => Map.unmodifiable(_extra); + /// Active replay recording. + @internal + SentryId? get replayId => _replayId; + @internal + set replayId(SentryId? value) => _replayId = value; + SentryId? _replayId; + final Contexts _contexts = Contexts(); /// Unmodifiable map of the scope contexts key/value @@ -237,6 +244,7 @@ class Scope { _tags.clear(); _extra.clear(); _eventProcessors.clear(); + _replayId = null; _clearBreadcrumbsSync(); _setUserSync(null); @@ -429,7 +437,8 @@ class Scope { ..fingerprint = List.from(fingerprint) .._transaction = _transaction ..span = span - .._enableScopeSync = false; + .._enableScopeSync = false + .._replayId = _replayId; clone._setUserSync(user); diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index ebed8765b1..b6fc8b7dac 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -111,6 +111,9 @@ class SentryBaggage { // ignore: deprecated_member_use_from_same_package setUserSegment(scope.user!.segment!); } + if (scope.replayId != null && scope.replayId != SentryId.empty()) { + setReplayId(scope.replayId.toString()); + } } static Map _extractKeyValuesFromBaggageString( @@ -205,5 +208,12 @@ class SentryBaggage { return double.tryParse(sampleRate); } + void setReplayId(String value) => set('sentry-replay_id', value); + + SentryId? getReplayId() { + final replayId = get('sentry-replay_id'); + return replayId == null ? null : SentryId.fromId(replayId); + } + Map get keyValues => Map.unmodifiable(_keyValues); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index b659d43a47..b66c0d25b5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -168,15 +168,15 @@ class SentryClient { var traceContext = scope?.span?.traceContext(); if (traceContext == null) { - if (scope?.propagationContext.baggage == null) { - scope?.propagationContext.baggage = - SentryBaggage({}, logger: _options.logger); - scope?.propagationContext.baggage?.setValuesFromScope(scope, _options); - } if (scope != null) { + scope.propagationContext.baggage ??= + SentryBaggage({}, logger: _options.logger) + ..setValuesFromScope(scope, _options); traceContext = SentryTraceContextHeader.fromBaggage( scope.propagationContext.baggage!); } + } else { + traceContext.replayId = scope?.replayId; } final envelope = SentryEnvelope.fromEvent( diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index ed7ec53653..e17b2b91f8 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -17,6 +17,7 @@ class SentryTraceContextHeader { this.sampleRate, this.sampled, this.unknown, + this.replayId, }); final SentryId traceId; @@ -34,6 +35,9 @@ class SentryTraceContextHeader { @internal final Map? unknown; + @internal + SentryId? replayId; + /// Deserializes a [SentryTraceContextHeader] from JSON [Map]. factory SentryTraceContextHeader.fromJson(Map data) { final json = AccessAwareMap(data); @@ -47,6 +51,8 @@ class SentryTraceContextHeader { transaction: json['transaction'], sampleRate: json['sample_rate'], sampled: json['sampled'], + replayId: + json['replay_id'] == null ? null : SentryId.fromId(json['replay_id']), unknown: json.notAccessed(), ); } @@ -65,6 +71,7 @@ class SentryTraceContextHeader { if (transaction != null) 'transaction': transaction, if (sampleRate != null) 'sample_rate': sampleRate, if (sampled != null) 'sampled': sampled, + if (replayId != null) 'replay_id': replayId.toString(), }; } @@ -98,6 +105,9 @@ class SentryTraceContextHeader { if (sampled != null) { baggage.setSampled(sampled!); } + if (replayId != null) { + baggage.setReplayId(replayId.toString()); + } return baggage; } @@ -107,6 +117,7 @@ class SentryTraceContextHeader { baggage.get('sentry-public_key').toString(), release: baggage.get('sentry-release'), environment: baggage.get('sentry-environment'), + replayId: baggage.getReplayId(), ); } } diff --git a/dart/lib/src/version.dart b/dart/lib/src/version.dart index 254e1efb4f..f3ba5d0742 100644 --- a/dart/lib/src/version.dart +++ b/dart/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '8.7.0'; +const String sdkVersion = '8.8.0'; String sdkName(bool isWeb) => isWeb ? _browserSdkName : _ioSdkName; diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 2783ac7038..036f2ca3aa 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 8.7.0 +version: 8.8.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/breadcrumb_test.dart b/dart/test/protocol/breadcrumb_test.dart index ddda79df6e..2dea300561 100644 --- a/dart/test/protocol/breadcrumb_test.dart +++ b/dart/test/protocol/breadcrumb_test.dart @@ -90,7 +90,7 @@ void main() { level: SentryLevel.fatal, reason: 'OK', statusCode: 200, - requestDuration: Duration.zero, + requestDuration: Duration(milliseconds: 55), timestamp: DateTime.now(), requestBodySize: 2, responseBodySize: 3, @@ -107,17 +107,43 @@ void main() { 'method': 'GET', 'status_code': 200, 'reason': 'OK', - 'duration': '0:00:00.000000', + 'duration': '0:00:00.055000', 'request_body_size': 2, 'response_body_size': 3, 'http.query': 'foo=bar', - 'http.fragment': 'baz' + 'http.fragment': 'baz', + 'start_timestamp': breadcrumb.timestamp.millisecondsSinceEpoch - 55, + 'end_timestamp': breadcrumb.timestamp.millisecondsSinceEpoch }, 'level': 'fatal', 'type': 'http', }); }); + test('Breadcrumb http', () { + final breadcrumb = Breadcrumb.http( + url: Uri.parse('https://example.org'), + method: 'GET', + requestDuration: Duration(milliseconds: 10), + ); + final json = breadcrumb.toJson(); + + expect(json, { + 'timestamp': + formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp), + 'category': 'http', + 'data': { + 'url': 'https://example.org', + 'method': 'GET', + 'duration': '0:00:00.010000', + 'start_timestamp': breadcrumb.timestamp.millisecondsSinceEpoch - 10, + 'end_timestamp': breadcrumb.timestamp.millisecondsSinceEpoch + }, + 'level': 'info', + 'type': 'http', + }); + }); + test('Minimal Breadcrumb http', () { final breadcrumb = Breadcrumb.http( url: Uri.parse('https://example.org'), @@ -196,6 +222,7 @@ void main() { 'foo': 'bar', 'view.id': 'foo', 'view.class': 'bar', + 'path': 'bar(foo)', }, }); }); diff --git a/dart/test/protocol/sentry_baggage_header_test.dart b/dart/test/protocol/sentry_baggage_header_test.dart index 3e8555aba9..910929776e 100644 --- a/dart/test/protocol/sentry_baggage_header_test.dart +++ b/dart/test/protocol/sentry_baggage_header_test.dart @@ -22,11 +22,23 @@ void main() { baggage.setTransaction('transaction'); baggage.setSampleRate('1.0'); baggage.setSampled('false'); + final replayId = SentryId.newId().toString(); + baggage.setReplayId(replayId); final baggageHeader = SentryBaggageHeader.fromBaggage(baggage); - expect(baggageHeader.value, - 'sentry-trace_id=$id,sentry-public_key=publicKey,sentry-release=release,sentry-environment=environment,sentry-user_id=userId,sentry-user_segment=userSegment,sentry-transaction=transaction,sentry-sample_rate=1.0,sentry-sampled=false'); + expect( + baggageHeader.value, + 'sentry-trace_id=$id,' + 'sentry-public_key=publicKey,' + 'sentry-release=release,' + 'sentry-environment=environment,' + 'sentry-user_id=userId,' + 'sentry-user_segment=userSegment,' + 'sentry-transaction=transaction,' + 'sentry-sample_rate=1.0,' + 'sentry-sampled=false,' + 'sentry-replay_id=$replayId'); }); }); } diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 8a535541c1..0059a2e77a 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -88,6 +88,14 @@ void main() { expect(sut.fingerprint, fingerprints); }); + test('sets replay ID', () { + final sut = fixture.getSut(); + + sut.replayId = SentryId.fromId('1'); + + expect(sut.replayId, SentryId.fromId('1')); + }); + test('adds $Breadcrumb', () { final sut = fixture.getSut(); @@ -307,6 +315,7 @@ void main() { sut.level = SentryLevel.debug; sut.transaction = 'test'; sut.span = null; + sut.replayId = SentryId.newId(); final user = SentryUser(id: 'test'); sut.setUser(user); @@ -322,21 +331,15 @@ void main() { sut.clear(); expect(sut.breadcrumbs.length, 0); - expect(sut.level, null); - expect(sut.transaction, null); expect(sut.span, null); - expect(sut.user, null); - expect(sut.fingerprint.length, 0); - expect(sut.tags.length, 0); - expect(sut.extra.length, 0); - expect(sut.eventProcessors.length, 0); + expect(sut.replayId, isNull); }); test('clones', () async { @@ -349,6 +352,7 @@ void main() { sut.addAttachment(SentryAttachment.fromIntList([0, 0, 0, 0], 'test.txt')); sut.span = NoOpSentrySpan(); sut.level = SentryLevel.warning; + sut.replayId = SentryId.newId(); await sut.setUser(SentryUser(id: 'id')); await sut.setTag('key', 'vakye'); await sut.setExtra('key', 'vakye'); @@ -369,6 +373,7 @@ void main() { true, ); expect(sut.span, clone.span); + expect(sut.replayId, clone.replayId); }); test('clone does not additionally call observers', () async { diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 772699b14d..07d5aab834 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -813,7 +813,8 @@ void main() { ..addBreadcrumb(crumb) ..setTag(scopeTagKey, scopeTagValue) // ignore: deprecated_member_use_from_same_package - ..setExtra(scopeExtraKey, scopeExtraValue); + ..setExtra(scopeExtraKey, scopeExtraValue) + ..replayId = SentryId.fromId('1'); scope.setUser(user); }); @@ -839,6 +840,8 @@ void main() { scopeExtraKey: scopeExtraValue, eventExtraKey: eventExtraValue, }); + expect( + capturedEnvelope.header.traceContext?.replayId, SentryId.fromId('1')); }); }); @@ -1485,6 +1488,7 @@ void main() { final client = fixture.getSut(); final scope = Scope(fixture.options); + scope.replayId = SentryId.newId(); scope.span = SentrySpan(fixture.tracer, fixture.tracer.context, MockHub()); @@ -1492,6 +1496,7 @@ void main() { final envelope = fixture.transport.envelopes.first; expect(envelope.header.traceContext, isNotNull); + expect(envelope.header.traceContext?.replayId, scope.replayId); }); test('captureEvent adds attachments from hint', () async { @@ -1548,12 +1553,14 @@ void main() { final context = SentryTraceContextHeader.fromJson({ 'trace_id': '${tr.eventId}', 'public_key': '123', + 'replay_id': '456', }); await client.captureTransaction(tr, traceContext: context); final envelope = fixture.transport.envelopes.first; expect(envelope.header.traceContext, isNotNull); + expect(envelope.header.traceContext?.replayId, SentryId.fromId('456')); }); test('captureUserFeedback calls flush', () async { diff --git a/dart/test/sentry_trace_context_header_test.dart b/dart/test/sentry_trace_context_header_test.dart index db9a2d3621..04b2526c34 100644 --- a/dart/test/sentry_trace_context_header_test.dart +++ b/dart/test/sentry_trace_context_header_test.dart @@ -18,6 +18,7 @@ void main() { transaction: 'transaction', sampleRate: '1.0', sampled: 'false', + replayId: SentryId.fromId('456'), unknown: testUnknown, ); @@ -30,7 +31,8 @@ void main() { 'user_segment': 'user_segment', 'transaction': 'transaction', 'sample_rate': '1.0', - 'sampled': 'false' + 'sampled': 'false', + 'replay_id': '456', }; mapJson.addAll(testUnknown); @@ -45,6 +47,7 @@ void main() { expect(context.transaction, 'transaction'); expect(context.sampleRate, '1.0'); expect(context.sampled, 'false'); + expect(context.replayId, SentryId.fromId('456')); }); test('toJson', () { @@ -56,8 +59,19 @@ void main() { test('to baggage', () { final baggage = context.toBaggage(); - expect(baggage.toHeaderString(), - 'sentry-trace_id=${traceId.toString()},sentry-public_key=123,sentry-release=release,sentry-environment=environment,sentry-user_id=user_id,sentry-user_segment=user_segment,sentry-transaction=transaction,sentry-sample_rate=1.0,sentry-sampled=false'); + expect( + baggage.toHeaderString(), + 'sentry-trace_id=${traceId.toString()},' + 'sentry-public_key=123,' + 'sentry-release=release,' + 'sentry-environment=environment,' + 'sentry-user_id=user_id,' + 'sentry-user_segment=user_segment,' + 'sentry-transaction=transaction,' + 'sentry-sample_rate=1.0,' + 'sentry-sampled=false,' + 'sentry-replay_id=456', + ); }); }); } diff --git a/dart/test/sentry_trace_context_test.dart b/dart/test/sentry_trace_context_test.dart index 9030b48182..ab33512a2c 100644 --- a/dart/test/sentry_trace_context_test.dart +++ b/dart/test/sentry_trace_context_test.dart @@ -18,28 +18,32 @@ void main() { expect(map['description'], 'desc'); expect(map['status'], 'aborted'); expect(map['origin'], 'auto.ui'); + expect(map['replay_id'], isNotNull); }); test('fromJson deserializes', () { final map = { 'op': 'op', - 'span_id': '0000000000000000', - 'trace_id': '00000000000000000000000000000000', - 'parent_span_id': '0000000000000000', + 'span_id': '0000000000000001', + 'trace_id': '00000000000000000000000000000002', + 'parent_span_id': '0000000000000003', 'description': 'desc', 'status': 'aborted', - 'origin': 'auto.ui' + 'origin': 'auto.ui', + 'replay_id': '00000000000000000000000000000004' }; map.addAll(testUnknown); final traceContext = SentryTraceContext.fromJson(map); expect(traceContext.description, 'desc'); expect(traceContext.operation, 'op'); - expect(traceContext.spanId.toString(), '0000000000000000'); - expect(traceContext.traceId.toString(), '00000000000000000000000000000000'); - expect(traceContext.parentSpanId.toString(), '0000000000000000'); + expect(traceContext.spanId.toString(), '0000000000000001'); + expect(traceContext.traceId.toString(), '00000000000000000000000000000002'); + expect(traceContext.parentSpanId.toString(), '0000000000000003'); expect(traceContext.status.toString(), 'aborted'); expect(traceContext.sampled, true); + expect( + traceContext.replayId.toString(), '00000000000000000000000000000004'); }); } @@ -52,6 +56,7 @@ class Fixture { sampled: true, status: SpanStatus.aborted(), origin: 'auto.ui', + replayId: SentryId.newId(), unknown: testUnknown, ); } diff --git a/dio/lib/src/version.dart b/dio/lib/src/version.dart index b8716dbb69..17d1f09e4c 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.7.0'; +const String sdkVersion = '8.8.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 48654aeccc..b5f9fcf3d3 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 dev_dependencies: meta: ^1.3.0 diff --git a/drift/lib/src/version.dart b/drift/lib/src/version.dart index 1ee2e5162d..b1086b5791 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.7.0'; +const String sdkVersion = '8.8.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 b289220ef8..1a6c4a96d6 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 meta: ^1.3.0 drift: ^2.13.0 @@ -32,4 +32,4 @@ dev_dependencies: yaml: ^3.1.0 # needed for version match (code and pubspec) sqlite3_flutter_libs: ^0.5.0 sqlite3: ^2.1.0 - archive: ^3.1.2 \ No newline at end of file + archive: ^3.1.2 diff --git a/file/lib/src/version.dart b/file/lib/src/version.dart index 67deaf2942..971d2bede1 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.7.0'; +const String sdkVersion = '8.8.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 6e837e4636..487c1cd3cf 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 meta: ^1.3.0 dev_dependencies: diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index 123778e9be..88d9016a47 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -3,6 +3,7 @@ package io.sentry.flutter import android.util.Log import io.sentry.SentryLevel import io.sentry.SentryOptions.Proxy +import io.sentry.SentryReplayOptions import io.sentry.android.core.BuildConfig import io.sentry.android.core.SentryAndroidOptions import io.sentry.protocol.SdkVersion @@ -146,6 +147,18 @@ class SentryFlutter( pass = proxyJson["pass"] as? String } } + + data.getIfNotNull>("replay") { + updateReplayOptions(options.experimental.sessionReplay, it) + } + } + + fun updateReplayOptions( + options: SentryReplayOptions, + data: Map, + ) { + options.sessionSampleRate = data["sessionSampleRate"] as? Double + options.errorSampleRate = data["errorSampleRate"] as? Double } } diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 4f154a2465..a10966fd50 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -3,6 +3,7 @@ package io.sentry.flutter import android.app.Activity import android.content.Context import android.os.Build +import android.os.Looper import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -25,16 +26,20 @@ import io.sentry.android.core.SentryAndroid import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.core.performance.TimeSpan +import io.sentry.android.replay.ReplayIntegration import io.sentry.protocol.DebugImage import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId import io.sentry.protocol.User +import io.sentry.transport.CurrentDateProvider +import java.io.File import java.lang.ref.WeakReference class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var channel: MethodChannel private lateinit var context: Context private lateinit var sentryFlutter: SentryFlutter + private lateinit var replay: ReplayIntegration private var activity: WeakReference? = null private var framesTracker: ActivityFramesTracker? = null @@ -49,12 +54,16 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { sentryFlutter = SentryFlutter( - androidSdk = androidSdk, - nativeSdk = nativeSdk, + androidSdk = ANDROID_SDK, + nativeSdk = NATIVE_SDK, ) } - override fun onMethodCall(call: MethodCall, result: Result) { + @Suppress("CyclomaticComplexMethod") + override fun onMethodCall( + call: MethodCall, + result: Result, + ) { when (call.method) { "initNativeSdk" -> initNativeSdk(call, result) "captureEnvelope" -> captureEnvelope(call, result) @@ -74,6 +83,9 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "removeTag" -> removeTag(call.argument("key"), result) "loadContexts" -> loadContexts(result) "displayRefreshRate" -> displayRefreshRate(result) + "nativeCrash" -> crash() + "addReplayScreenshot" -> addReplayScreenshot(call.argument("path"), call.argument("timestamp"), result) + "captureReplay" -> captureReplay(call.argument("isCrash"), result) else -> result.notImplemented() } } @@ -103,7 +115,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { // Stub } - private fun initNativeSdk(call: MethodCall, result: Result) { + private fun initNativeSdk( + call: MethodCall, + result: Result, + ) { if (!this::context.isInitialized) { result.error("1", "Context is null", null) return @@ -123,6 +138,27 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } options.beforeSend = BeforeSendCallbackImpl(options.sdkVersion) + + // Replace the default ReplayIntegration with a Flutter-specific recorder. + options.integrations.removeAll { it is ReplayIntegration } + val cacheDirPath = options.cacheDirPath + val replayOptions = options.experimental.sessionReplay + val isReplayEnabled = replayOptions.isSessionReplayEnabled || replayOptions.isSessionReplayForErrorsEnabled + if (cacheDirPath != null && isReplayEnabled) { + replay = + ReplayIntegration( + context, + dateProvider = CurrentDateProvider.getInstance(), + recorderProvider = { SentryFlutterReplayRecorder(channel, replay) }, + recorderConfigProvider = null, + replayCacheProvider = null, + ) + replay.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter() + options.addIntegration(replay) + options.setReplayController(replay) + } else { + options.setReplayController(null) + } } result.success("") } @@ -145,6 +181,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } else { val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble()) val item = + mutableMapOf( "pluginRegistrationTime" to pluginRegistrationTime, "appStartTime" to appStartTimeMillis, @@ -228,7 +265,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success(null) } - private fun endNativeFrames(id: String?, result: Result) { + private fun endNativeFrames( + id: String?, + result: Result, + ) { val activity = activity?.get() if (!sentryFlutter.autoPerformanceTracingEnabled || activity == null || id == null) { if (id == null) { @@ -248,16 +288,21 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { if (total == 0 && slow == 0 && frozen == 0) { result.success(null) } else { - val frames = mapOf( - "totalFrames" to total, - "slowFrames" to slow, - "frozenFrames" to frozen, - ) + val frames = + mapOf( + "totalFrames" to total, + "slowFrames" to slow, + "frozenFrames" to frozen, + ) result.success(frames) } } - private fun setContexts(key: String?, value: Any?, result: Result) { + private fun setContexts( + key: String?, + value: Any?, + result: Result, + ) { if (key == null || value == null) { result.success("") return @@ -269,7 +314,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } - private fun removeContexts(key: String?, result: Result) { + private fun removeContexts( + key: String?, + result: Result, + ) { if (key == null) { result.success("") return @@ -281,7 +329,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } - private fun setUser(user: Map?, result: Result) { + private fun setUser( + user: Map?, + result: Result, + ) { if (user != null) { val options = HubAdapter.getInstance().options val userInstance = User.fromMap(user, options) @@ -292,7 +343,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun addBreadcrumb(breadcrumb: Map?, result: Result) { + private fun addBreadcrumb( + breadcrumb: Map?, + result: Result, + ) { if (breadcrumb != null) { val options = HubAdapter.getInstance().options val breadcrumbInstance = Breadcrumb.fromMap(breadcrumb, options) @@ -307,7 +361,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun setExtra(key: String?, value: String?, result: Result) { + private fun setExtra( + key: String?, + value: String?, + result: Result, + ) { if (key == null || value == null) { result.success("") return @@ -317,7 +375,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun removeExtra(key: String?, result: Result) { + private fun removeExtra( + key: String?, + result: Result, + ) { if (key == null) { result.success("") return @@ -327,7 +388,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun setTag(key: String?, value: String?, result: Result) { + private fun setTag( + key: String?, + value: String?, + result: Result, + ) { if (key == null || value == null) { result.success("") return @@ -337,7 +402,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun removeTag(key: String?, result: Result) { + private fun removeTag( + key: String?, + result: Result, + ) { if (key == null) { result.success("") return @@ -347,7 +415,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { result.success("") } - private fun captureEnvelope(call: MethodCall, result: Result) { + private fun captureEnvelope( + call: MethodCall, + result: Result, + ) { if (!Sentry.isEnabled()) { result.error("1", "The Sentry Android SDK is disabled", null) return @@ -356,7 +427,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { if (args.isNotEmpty()) { val event = args.first() as ByteArray? val containsUnhandledException = args[1] as Boolean - if (event != null && event.isNotEmpty() && containsUnhandledException != null) { + if (event != null && event.isNotEmpty()) { val id = InternalSentrySdk.captureEnvelope(event, containsUnhandledException) if (id != null) { result.success("") @@ -405,7 +476,10 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private class BeforeSendCallbackImpl( private val sdkVersion: SdkVersion?, ) : SentryOptions.BeforeSendCallback { - override fun execute(event: SentryEvent, hint: Hint): SentryEvent { + override fun execute( + event: SentryEvent, + hint: Hint, + ): SentryEvent { setEventOriginTag(event) addPackages(event, sdkVersion) return event @@ -413,16 +487,17 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } companion object { + private const val FLUTTER_SDK = "sentry.dart.flutter" + private const val ANDROID_SDK = "sentry.java.android.flutter" + private const val NATIVE_SDK = "sentry.native.android.flutter" + private const val NATIVE_CRASH_WAIT_TIME = 500L - private const val flutterSdk = "sentry.dart.flutter" - private const val androidSdk = "sentry.java.android.flutter" - private const val nativeSdk = "sentry.native.android.flutter" private fun setEventOriginTag(event: SentryEvent) { event.sdk?.let { when (it.name) { - flutterSdk -> setEventEnvironmentTag(event, "flutter", "dart") - androidSdk -> setEventEnvironmentTag(event, environment = "java") - nativeSdk -> setEventEnvironmentTag(event, environment = "native") + FLUTTER_SDK -> setEventEnvironmentTag(event, "flutter", "dart") + ANDROID_SDK -> setEventEnvironmentTag(event, environment = "java") + NATIVE_SDK -> setEventEnvironmentTag(event, environment = "native") else -> return } } @@ -437,9 +512,12 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { event.setTag("event.environment", environment) } - private fun addPackages(event: SentryEvent, sdk: SdkVersion?) { + private fun addPackages( + event: SentryEvent, + sdk: SdkVersion?, + ) { event.sdk?.let { - if (it.name == flutterSdk) { + if (it.name == FLUTTER_SDK) { sdk?.packageSet?.forEach { sentryPackage -> it.addPackage(sentryPackage.name, sentryPackage.version) } @@ -449,6 +527,13 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } } + + private fun crash() { + val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") + val mainThread = Looper.getMainLooper().thread + mainThread.uncaughtExceptionHandler.uncaughtException(mainThread, exception) + mainThread.join(NATIVE_CRASH_WAIT_TIME) + } } private fun loadContexts(result: Result) { @@ -466,4 +551,29 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { ) result.success(serializedScope) } + + private fun addReplayScreenshot( + path: String?, + timestamp: Long?, + result: Result, + ) { + if (path == null || timestamp == null) { + result.error("5", "Arguments are null", null) + return + } + replay.onScreenshotRecorded(File(path), timestamp) + result.success("") + } + + private fun captureReplay( + isCrash: Boolean?, + result: Result, + ) { + if (isCrash == null) { + result.error("5", "Arguments are null", null) + return + } + replay.captureReplay(isCrash) + result.success(replay.getReplayId().toString()) + } } diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayBreadcrumbConverter.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayBreadcrumbConverter.kt new file mode 100644 index 0000000000..3dd549802f --- /dev/null +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayBreadcrumbConverter.kt @@ -0,0 +1,86 @@ +package io.sentry.flutter + +import io.sentry.Breadcrumb +import io.sentry.android.replay.DefaultReplayBreadcrumbConverter +import io.sentry.rrweb.RRWebBreadcrumbEvent +import io.sentry.rrweb.RRWebEvent +import io.sentry.rrweb.RRWebSpanEvent +import java.util.Date + +private const val MILLIS_PER_SECOND = 1000.0 + +class SentryFlutterReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter() { + internal companion object { + private val supportedNetworkData = + mapOf( + "status_code" to "statusCode", + "method" to "method", + "response_body_size" to "responseBodySize", + "request_body_size" to "requestBodySize", + ) + } + + override fun convert(breadcrumb: Breadcrumb): RRWebEvent? { + return when (breadcrumb.category) { + null -> null + "sentry.event" -> null + "sentry.transaction" -> null + "http" -> convertNetworkBreadcrumb(breadcrumb) + "navigation" -> newRRWebBreadcrumb(breadcrumb) + "ui.click" -> + newRRWebBreadcrumb(breadcrumb).apply { + category = "ui.tap" + message = breadcrumb.data["path"] as String? + } + + else -> { + val nativeBreadcrumb = super.convert(breadcrumb) + + // ignore native navigation breadcrumbs + if (nativeBreadcrumb is RRWebBreadcrumbEvent) { + if (nativeBreadcrumb.category == "navigation") { + return null + } + } + + nativeBreadcrumb + } + } + } + + private fun newRRWebBreadcrumb(breadcrumb: Breadcrumb): RRWebBreadcrumbEvent = + RRWebBreadcrumbEvent().apply { + category = breadcrumb.category + level = breadcrumb.level + data = breadcrumb.data + timestamp = breadcrumb.timestamp.time + breadcrumbTimestamp = doubleTimestamp(breadcrumb.timestamp) + breadcrumbType = "default" + } + + private fun doubleTimestamp(date: Date) = doubleTimestamp(date.time) + + private fun doubleTimestamp(timestamp: Long) = timestamp / MILLIS_PER_SECOND + + private fun convertNetworkBreadcrumb(breadcrumb: Breadcrumb): RRWebEvent? { + var rrWebEvent = super.convert(breadcrumb) + if (rrWebEvent == null && + breadcrumb.data.containsKey("start_timestamp") && + breadcrumb.data.containsKey("end_timestamp") + ) { + rrWebEvent = + RRWebSpanEvent().apply { + op = "resource.http" + timestamp = breadcrumb.timestamp.time + description = breadcrumb.data["url"] as String + startTimestamp = doubleTimestamp(breadcrumb.data["start_timestamp"] as Long) + endTimestamp = doubleTimestamp(breadcrumb.data["end_timestamp"] as Long) + data = + breadcrumb.data + .filterKeys { key -> supportedNetworkData.containsKey(key) } + .mapKeys { (key, _) -> supportedNetworkData[key] } + } + } + return rrWebEvent + } +} diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt new file mode 100644 index 0000000000..41209f75b6 --- /dev/null +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -0,0 +1,72 @@ +package io.sentry.flutter + +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.flutter.plugin.common.MethodChannel +import io.sentry.android.replay.Recorder +import io.sentry.android.replay.ReplayIntegration +import io.sentry.android.replay.ScreenshotRecorderConfig + +internal class SentryFlutterReplayRecorder( + private val channel: MethodChannel, + private val integration: ReplayIntegration, +) : Recorder { + override fun start(config: ScreenshotRecorderConfig) { + val cacheDirPath = integration.replayCacheDir?.absolutePath + if (cacheDirPath == null) { + Log.w("Sentry", "Replay cache directory is null, can't start replay recorder.") + return + } + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod( + "ReplayRecorder.start", + mapOf( + "directory" to cacheDirPath, + "width" to config.recordingWidth, + "height" to config.recordingHeight, + "frameRate" to config.frameRate, + "replayId" to integration.getReplayId().toString(), + ), + ) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to start replay recorder", ignored) + } + } + } + + override fun resume() { + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod("ReplayRecorder.resume", null) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to resume replay recorder", ignored) + } + } + } + + override fun pause() { + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod("ReplayRecorder.pause", null) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to pause replay recorder", ignored) + } + } + } + + override fun stop() { + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod("ReplayRecorder.stop", null) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to stop replay recorder", ignored) + } + } + } + + override fun close() { + stop() + } +} diff --git a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt index 9fa9183f33..c43b2807ed 100644 --- a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt +++ b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt @@ -67,6 +67,16 @@ class SentryFlutterTest { assertEquals(Proxy.Type.HTTP, fixture.options.proxy?.type) assertEquals("admin", fixture.options.proxy?.user) assertEquals("0000", fixture.options.proxy?.pass) + + assertEquals(0.5, fixture.options.experimental.sessionReplay.sessionSampleRate) + assertEquals(0.6, fixture.options.experimental.sessionReplay.errorSampleRate) + + // Note: these are currently read-only in SentryReplayOptions so we're only asserting the default values here to + // know when there's a change in the native SDK, as it may require a manual change in the Flutter implementation. + assertEquals(1, fixture.options.experimental.sessionReplay.frameRate) + assertEquals(30_000L, fixture.options.experimental.sessionReplay.errorReplayDuration) + assertEquals(5000L, fixture.options.experimental.sessionReplay.sessionSegmentDuration) + assertEquals(60 * 60 * 1000L, fixture.options.experimental.sessionReplay.sessionDuration) } @Test @@ -142,6 +152,11 @@ class Fixture { "user" to "admin", "pass" to "0000", ), + "replay" to + mapOf( + "sessionSampleRate" to 0.5, + "errorSampleRate" to 0.6, + ), ) fun getSut(): SentryFlutter = diff --git a/flutter/example/android/app/build.gradle b/flutter/example/android/app/build.gradle index e9ac4161a5..ed3e1a1b6b 100644 --- a/flutter/example/android/app/build.gradle +++ b/flutter/example/android/app/build.gradle @@ -65,8 +65,6 @@ android { } } - // TODO: we need to fix CI as the version 21.1 (default) is not installed by default on - // GH Actions. ndkVersion "25.1.8937393" externalNativeBuild { diff --git a/flutter/example/android/app/src/main/AndroidManifest.xml b/flutter/example/android/app/src/main/AndroidManifest.xml index c2029920e9..1b2b2012cf 100644 --- a/flutter/example/android/app/src/main/AndroidManifest.xml +++ b/flutter/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + + + setupSentry( options.maxResponseBodySize = MaxResponseBodySize.always; options.navigatorKey = navigatorKey; + options.experimental.replay.sessionSampleRate = 1.0; + options.experimental.replay.errorSampleRate = 1.0; + _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { options.dist = '1'; @@ -758,6 +761,12 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Platform exception'), ), + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ]); } } @@ -870,6 +879,12 @@ class CocoaExample extends StatelessWidget { }, child: const Text('Objective-C SEGFAULT'), ), + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ], ); } diff --git a/flutter/example/pubspec.yaml b/flutter/example/pubspec.yaml index 88919c32a6..dae5c41ece 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.7.0 +version: 8.8.0 publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/flutter/example/windows/CMakeLists.txt b/flutter/example/windows/CMakeLists.txt index 845ddf6fef..5a554e25d0 100644 --- a/flutter/example/windows/CMakeLists.txt +++ b/flutter/example/windows/CMakeLists.txt @@ -1,13 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) project(sentry_flutter_example LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "sentry_flutter_example") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -30,6 +33,10 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/flutter/example/windows/flutter/CMakeLists.txt b/flutter/example/windows/flutter/CMakeLists.txt index c10f4f62cb..efb62ebe7d 100644 --- a/flutter/example/windows/flutter/CMakeLists.txt +++ b/flutter/example/windows/flutter/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.15) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/flutter/example/windows/runner/CMakeLists.txt b/flutter/example/windows/runner/CMakeLists.txt index e993217632..2041a04410 100644 --- a/flutter/example/windows/runner/CMakeLists.txt +++ b/flutter/example/windows/runner/CMakeLists.txt @@ -1,18 +1,40 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" - "run_loop.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/example/windows/runner/Runner.rc b/flutter/example/windows/runner/Runner.rc index a059a08d0d..90e64b7b0f 100644 --- a/flutter/example/windows/runner/Runner.rc +++ b/flutter/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif @@ -90,10 +90,10 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "Demonstrates how to use the sentry_flutter plugin." "\0" + VALUE "FileDescription", "sentry_flutter_example" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "sentry_flutter_example" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "sentry_flutter_example.exe" "\0" VALUE "ProductName", "sentry_flutter_example" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/flutter/example/windows/runner/flutter_window.cpp b/flutter/example/windows/runner/flutter_window.cpp index ac04f7790a..c819cb083f 100644 --- a/flutter/example/windows/runner/flutter_window.cpp +++ b/flutter/example/windows/runner/flutter_window.cpp @@ -4,9 +4,8 @@ #include "flutter/generated_plugin_registrant.h" -FlutterWindow::FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project) - : run_loop_(run_loop), project_(project) {} +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} FlutterWindow::~FlutterWindow() {} @@ -26,14 +25,22 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); - run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { - run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); flutter_controller_ = nullptr; } @@ -44,7 +51,7 @@ LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opporutunity to handle window messages. + // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, diff --git a/flutter/example/windows/runner/flutter_window.h b/flutter/example/windows/runner/flutter_window.h index ba86031c6c..28c23839b9 100644 --- a/flutter/example/windows/runner/flutter_window.h +++ b/flutter/example/windows/runner/flutter_window.h @@ -6,16 +6,13 @@ #include -#include "run_loop.h" #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: - // Creates a new FlutterWindow driven by the |run_loop|, hosting a - // Flutter view running |project|. - explicit FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project); + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: @@ -26,9 +23,6 @@ class FlutterWindow : public Win32Window { LPARAM const lparam) noexcept override; private: - // The run loop driving events for this window. - RunLoop* run_loop_; - // The project to run. flutter::DartProject project_; diff --git a/flutter/example/windows/runner/main.cpp b/flutter/example/windows/runner/main.cpp index 0685ffa6aa..11ea9c69a7 100644 --- a/flutter/example/windows/runner/main.cpp +++ b/flutter/example/windows/runner/main.cpp @@ -3,7 +3,6 @@ #include #include "flutter_window.h" -#include "run_loop.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -18,8 +17,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - RunLoop run_loop; - flutter::DartProject project(L"data"); std::vector command_line_arguments = @@ -27,15 +24,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - FlutterWindow window(&run_loop, project); + FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"sentry_flutter_example", origin, size)) { + if (!window.Create(L"sentry_flutter_example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); - run_loop.Run(); + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } ::CoUninitialize(); return EXIT_SUCCESS; diff --git a/flutter/example/windows/runner/run_loop.cpp b/flutter/example/windows/runner/run_loop.cpp deleted file mode 100644 index 0d912118c2..0000000000 --- a/flutter/example/windows/runner/run_loop.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "run_loop.h" - -#include - -#include - -RunLoop::RunLoop() {} - -RunLoop::~RunLoop() {} - -void RunLoop::Run() { - bool keep_running = true; - TimePoint next_flutter_event_time = TimePoint::clock::now(); - while (keep_running) { - std::chrono::nanoseconds wait_duration = - std::max(std::chrono::nanoseconds(0), - next_flutter_event_time - TimePoint::clock::now()); - ::MsgWaitForMultipleObjects( - 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), - QS_ALLINPUT); - bool processed_events = false; - MSG message; - // All pending Windows messages must be processed; MsgWaitForMultipleObjects - // won't return again for items left in the queue after PeekMessage. - while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { - processed_events = true; - if (message.message == WM_QUIT) { - keep_running = false; - break; - } - ::TranslateMessage(&message); - ::DispatchMessage(&message); - // Allow Flutter to process messages each time a Windows message is - // processed, to prevent starvation. - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - // If the PeekMessage loop didn't run, process Flutter messages. - if (!processed_events) { - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - } -} - -void RunLoop::RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.insert(flutter_instance); -} - -void RunLoop::UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.erase(flutter_instance); -} - -RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { - TimePoint next_event_time = TimePoint::max(); - for (auto instance : flutter_instances_) { - std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); - if (wait_duration != std::chrono::nanoseconds::max()) { - next_event_time = - std::min(next_event_time, TimePoint::clock::now() + wait_duration); - } - } - return next_event_time; -} diff --git a/flutter/example/windows/runner/run_loop.h b/flutter/example/windows/runner/run_loop.h deleted file mode 100644 index 54927f9773..0000000000 --- a/flutter/example/windows/runner/run_loop.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef RUNNER_RUN_LOOP_H_ -#define RUNNER_RUN_LOOP_H_ - -#include - -#include -#include - -// A runloop that will service events for Flutter instances as well -// as native messages. -class RunLoop { - public: - RunLoop(); - ~RunLoop(); - - // Prevent copying - RunLoop(RunLoop const&) = delete; - RunLoop& operator=(RunLoop const&) = delete; - - // Runs the run loop until the application quits. - void Run(); - - // Registers the given Flutter instance for event servicing. - void RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - // Unregisters the given Flutter instance from event servicing. - void UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - private: - using TimePoint = std::chrono::steady_clock::time_point; - - // Processes all currently pending messages for registered Flutter instances. - TimePoint ProcessFlutterMessages(); - - std::set flutter_instances_; -}; - -#endif // RUNNER_RUN_LOOP_H_ diff --git a/flutter/example/windows/runner/runner.exe.manifest b/flutter/example/windows/runner/runner.exe.manifest index 2c680b8be2..157e871fe8 100644 --- a/flutter/example/windows/runner/runner.exe.manifest +++ b/flutter/example/windows/runner/runner.exe.manifest @@ -7,7 +7,7 @@ - + diff --git a/flutter/example/windows/runner/utils.cpp b/flutter/example/windows/runner/utils.cpp index 05b53c01b4..fc55c573b5 100644 --- a/flutter/example/windows/runner/utils.cpp +++ b/flutter/example/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/flutter/example/windows/runner/win32_window.cpp b/flutter/example/windows/runner/win32_window.cpp index 97f4439cd1..b5ba2a099f 100644 --- a/flutter/example/windows/runner/win32_window.cpp +++ b/flutter/example/windows/runner/win32_window.cpp @@ -1,13 +1,31 @@ #include "win32_window.h" +#include #include #include "resource.h" namespace { +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; @@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); } + FreeLibrary(user32_module); } } // namespace @@ -42,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -102,9 +120,9 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); const wchar_t* window_class = @@ -117,7 +135,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); @@ -126,9 +144,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title, return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, @@ -188,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -243,3 +271,18 @@ bool Win32Window::OnCreate() { void Win32Window::OnDestroy() { // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/flutter/example/windows/runner/win32_window.h b/flutter/example/windows/runner/win32_window.h index d9bcac1b60..49b847f075 100644 --- a/flutter/example/windows/runner/win32_window.h +++ b/flutter/example/windows/runner/win32_window.h @@ -28,15 +28,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -76,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -86,6 +87,9 @@ class Win32Window { // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + bool quit_on_close_ = false; // window handle for top level window. diff --git a/flutter/ios/Classes/SentryFlutter.swift b/flutter/ios/Classes/SentryFlutter.swift index 769f595fba..3f77fad598 100644 --- a/flutter/ios/Classes/SentryFlutter.swift +++ b/flutter/ios/Classes/SentryFlutter.swift @@ -105,6 +105,14 @@ public final class SentryFlutter { options.urlSession = URLSession(configuration: configuration) } +#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) + if let replayOptions = data["replay"] as? [String: Any] { + options.experimental.sessionReplay.sessionSampleRate = + (replayOptions["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0 + options.experimental.sessionReplay.onErrorSampleRate = + (replayOptions["errorSampleRate"] as? NSNumber)?.floatValue ?? 0 + } +#endif } private func logLevelFrom(diagnosticLevel: String) -> SentryLevel { diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 35249ef5d1..3e436c88bb 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -12,6 +12,7 @@ import CoreVideo // swiftlint:disable:next type_body_length public class SentryFlutterPluginApple: NSObject, FlutterPlugin { + private let channel: FlutterMethodChannel private static let nativeClientName = "sentry.cocoa.flutter" @@ -38,12 +39,16 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { let channel = FlutterMethodChannel(name: "sentry_flutter", binaryMessenger: registrar.messenger) #endif - let instance = SentryFlutterPluginApple() + let instance = SentryFlutterPluginApple(channel: channel) instance.registerObserver() - registrar.addMethodCallDelegate(instance, channel: channel) } + private init(channel: FlutterMethodChannel) { + self.channel = channel + super.init() + } + private lazy var sentryFlutter = SentryFlutter() private func registerObserver() { @@ -174,6 +179,17 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { case "resumeAppHangTracking": resumeAppHangTracking(result) + case "nativeCrash": + crash() + + case "sendReplayForEvent": +#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS)) + PrivateSentrySDKOnly.captureReplay() + result(PrivateSentrySDKOnly.getReplayId()) +#else + result(nil) +#endif + default: result(FlutterMethodNotImplemented) } @@ -323,6 +339,14 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { didReceiveDidBecomeActiveNotification = false } +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + let breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter() + let screenshotProvider = SentryFlutterReplayScreenshotProvider(channel: self.channel) + PrivateSentrySDKOnly.configureSessionReplay(with: breadcrumbConverter, screenshotProvider: screenshotProvider) +#endif +#endif + result("") } @@ -729,6 +753,10 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { SentrySDK.resumeAppHangTracking() result("") } + + private func crash() { + SentrySDK.crash() + } } // swiftlint:enable function_body_length diff --git a/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.h b/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.h new file mode 100644 index 0000000000..1260268ced --- /dev/null +++ b/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.h @@ -0,0 +1,15 @@ +@import Sentry; + +#if SENTRY_TARGET_REPLAY_SUPPORTED +@class SentryRRWebEvent; + +@interface SentryFlutterReplayBreadcrumbConverter + : NSObject + +- (instancetype _Nonnull)init; + +- (id _Nullable)convertFrom: + (SentryBreadcrumb *_Nonnull)breadcrumb; + +@end +#endif diff --git a/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.m b/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.m new file mode 100644 index 0000000000..75b073de82 --- /dev/null +++ b/flutter/ios/Classes/SentryFlutterReplayBreadcrumbConverter.m @@ -0,0 +1,117 @@ +#import "SentryFlutterReplayBreadcrumbConverter.h" + +@import Sentry; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + +@implementation SentryFlutterReplayBreadcrumbConverter { + SentrySRDefaultBreadcrumbConverter *defaultConverter; +} + +- (instancetype _Nonnull)init { + if (self = [super init]) { + self->defaultConverter = + [SentrySessionReplayIntegration createDefaultBreadcrumbConverter]; + } + return self; +} + +- (id _Nullable)convertFrom: + (SentryBreadcrumb *_Nonnull)breadcrumb { + assert(breadcrumb.timestamp != nil); + + if (breadcrumb.category == nil + // Do not add Sentry Event breadcrumbs to replay + || [breadcrumb.category isEqualToString:@"sentry.event"] || + [breadcrumb.category isEqualToString:@"sentry.transaction"]) { + return nil; + } + + if ([breadcrumb.category isEqualToString:@"http"]) { + return [self convertNetwork:breadcrumb]; + } + + if ([breadcrumb.category isEqualToString:@"navigation"]) { + return [self convertFrom:breadcrumb withCategory:nil andMessage:nil]; + } + + if ([breadcrumb.category isEqualToString:@"ui.click"]) { + return [self convertFrom:breadcrumb + withCategory:@"ui.tap" + andMessage:breadcrumb.data[@"path"]]; + } + + SentryRRWebEvent *nativeBreadcrumb = + [self->defaultConverter convertFrom:breadcrumb]; + + // ignore native navigation breadcrumbs + if (nativeBreadcrumb && nativeBreadcrumb.data && + nativeBreadcrumb.data[@"payload"] && + nativeBreadcrumb.data[@"payload"][@"category"] && + [nativeBreadcrumb.data[@"payload"][@"category"] + isEqualToString:@"navigation"]) { + return nil; + } + + return nativeBreadcrumb; +} + +- (id _Nullable)convertFrom: + (SentryBreadcrumb *_Nonnull)breadcrumb + withCategory:(NSString *)category + andMessage:(NSString *)message { + return [SentrySessionReplayIntegration + createBreadcrumbwithTimestamp:breadcrumb.timestamp + category:category ?: breadcrumb.category + message:message ?: breadcrumb.message + level:breadcrumb.level + data:breadcrumb.data]; +} + +- (id _Nullable)convertNetwork: + (SentryBreadcrumb *_Nonnull)breadcrumb { + NSNumber *startTimestamp = + [breadcrumb.data[@"start_timestamp"] isKindOfClass:[NSNumber class]] + ? breadcrumb.data[@"start_timestamp"] + : nil; + NSNumber *endTimestamp = + [breadcrumb.data[@"end_timestamp"] isKindOfClass:[NSNumber class]] + ? breadcrumb.data[@"end_timestamp"] + : nil; + NSString *url = [breadcrumb.data[@"url"] isKindOfClass:[NSString class]] + ? breadcrumb.data[@"url"] + : nil; + + if (startTimestamp == nil || endTimestamp == nil || url == nil) { + return nil; + } + + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + if ([breadcrumb.data[@"method"] isKindOfClass:[NSString class]]) { + data[@"method"] = breadcrumb.data[@"method"]; + } + if ([breadcrumb.data[@"status_code"] isKindOfClass:[NSNumber class]]) { + data[@"statusCode"] = breadcrumb.data[@"status_code"]; + } + if ([breadcrumb.data[@"request_body_size"] isKindOfClass:[NSNumber class]]) { + data[@"requestBodySize"] = breadcrumb.data[@"request_body_size"]; + } + if ([breadcrumb.data[@"response_body_size"] isKindOfClass:[NSNumber class]]) { + data[@"responseBodySize"] = breadcrumb.data[@"response_body_size"]; + } + + return [SentrySessionReplayIntegration + createNetworkBreadcrumbWithTimestamp:[self dateFrom:startTimestamp] + endTimestamp:[self dateFrom:endTimestamp] + operation:@"resource.http" + description:url + data:data]; +} + +- (NSDate *_Nonnull)dateFrom:(NSNumber *_Nonnull)timestamp { + return [NSDate dateWithTimeIntervalSince1970:(timestamp.doubleValue / 1000)]; +} + +@end + +#endif diff --git a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.h b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.h new file mode 100644 index 0000000000..d59e5f4612 --- /dev/null +++ b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.h @@ -0,0 +1,12 @@ +@import Sentry; + +#if SENTRY_TARGET_REPLAY_SUPPORTED +@class SentryRRWebEvent; + +@interface SentryFlutterReplayScreenshotProvider + : NSObject + +- (instancetype)initWithChannel:(id)FlutterMethodChannel; + +@end +#endif diff --git a/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m new file mode 100644 index 0000000000..fc03fd5365 --- /dev/null +++ b/flutter/ios/Classes/SentryFlutterReplayScreenshotProvider.m @@ -0,0 +1,46 @@ +@import Sentry; + +#if SENTRY_TARGET_REPLAY_SUPPORTED +#import "SentryFlutterReplayScreenshotProvider.h" +#import + +@implementation SentryFlutterReplayScreenshotProvider { + FlutterMethodChannel *channel; +} + +- (instancetype _Nonnull)initWithChannel: + (FlutterMethodChannel *_Nonnull)channel { + if (self = [super init]) { + self->channel = channel; + } + return self; +} + +- (void)imageWithView:(UIView *_Nonnull)view + options:(id _Nonnull)options + onComplete:(void (^_Nonnull)(UIImage *_Nonnull))onComplete { + [self->channel + invokeMethod:@"captureReplayScreenshot" + arguments:@{@"replayId" : [PrivateSentrySDKOnly getReplayId]} + result:^(id value) { + if (value == nil) { + NSLog(@"SentryFlutterReplayScreenshotProvider received null " + @"result. " + @"Cannot capture a replay screenshot."); + } else if ([value + isKindOfClass:[FlutterStandardTypedData class]]) { + FlutterStandardTypedData *typedData = + (FlutterStandardTypedData *)value; + UIImage *image = [UIImage imageWithData:typedData.data]; + onComplete(image); + } else { + NSLog(@"SentryFlutterReplayScreenshotProvider received an " + @"unexpected result. " + @"Cannot capture a replay screenshot."); + } + }]; +} + +@end + +#endif diff --git a/flutter/ios/sentry_flutter.podspec b/flutter/ios/sentry_flutter.podspec index b75c20b16f..86a06e8179 100644 --- a/flutter/ios/sentry_flutter.podspec +++ b/flutter/ios/sentry_flutter.podspec @@ -16,7 +16,7 @@ Sentry SDK for Flutter with support to native through sentry-cocoa. :tag => s.version.to_s } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' - s.dependency 'Sentry/HybridSDK', '8.33.0' + s.dependency 'Sentry/HybridSDK', '8.36.0' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '12.0' diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index d15c8b7a70..bea9016630 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -8,6 +8,7 @@ export 'src/integrations/load_release_integration.dart'; export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; +export 'src/sentry_replay_options.dart'; export 'src/flutter_sentry_attachment.dart'; export 'src/sentry_asset_bundle.dart'; export 'src/integrations/on_error_integration.dart'; diff --git a/flutter/lib/src/event_processor/replay_event_processor.dart b/flutter/lib/src/event_processor/replay_event_processor.dart new file mode 100644 index 0000000000..1d534f94b0 --- /dev/null +++ b/flutter/lib/src/event_processor/replay_event_processor.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; + +import '../native/sentry_native_binding.dart'; + +class ReplayEventProcessor implements EventProcessor { + final SentryNativeBinding _binding; + + ReplayEventProcessor(this._binding); + + @override + Future apply(SentryEvent event, Hint hint) async { + if (event.eventId != SentryId.empty() && + event.exceptions?.isNotEmpty == true) { + final isCrash = + event.exceptions!.any((e) => e.mechanism?.handled == false); + await _binding.captureReplay(isCrash); + } + return event; + } +} diff --git a/flutter/lib/src/integrations/native_sdk_integration.dart b/flutter/lib/src/integrations/native_sdk_integration.dart index 7178883d73..ad77711b63 100644 --- a/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/flutter/lib/src/integrations/native_sdk_integration.dart @@ -20,7 +20,7 @@ class NativeSdkIntegration implements Integration { } try { - await _native.init(options); + await _native.init(hub); options.sdk.addIntegration('nativeSdkIntegration'); } catch (exception, stackTrace) { options.logger( diff --git a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index 9bb5af98b6..5666246472 100644 --- a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -1,17 +1,76 @@ import 'dart:ffi'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; +import '../../event_processor/replay_event_processor.dart'; +import '../../replay/recorder.dart'; +import '../../replay/recorder_config.dart'; import '../sentry_native_channel.dart'; import 'binding.dart' as cocoa; @internal class SentryNativeCocoa extends SentryNativeChannel { late final _lib = cocoa.SentryCocoa(DynamicLibrary.process()); + ScreenshotRecorder? _replayRecorder; + SentryId? _replayId; SentryNativeCocoa(super.options, super.channel); + @override + Future init(Hub hub) async { + // We only need these when replay is enabled (session or error capture) + // so let's set it up conditionally. This allows Dart to trim the code. + if (options.experimental.replay.isEnabled && + options.platformChecker.platform.isIOS) { + // We only need the integration when error-replay capture is enabled. + if ((options.experimental.replay.errorSampleRate ?? 0) > 0) { + options.addEventProcessor(ReplayEventProcessor(this)); + } + + channel.setMethodCallHandler((call) async { + switch (call.method) { + case 'captureReplayScreenshot': + _replayRecorder ??= + ScreenshotRecorder(ScreenshotRecorderConfig(), options); + final replayId = + SentryId.fromId(call.arguments['replayId'] as String); + if (_replayId != replayId) { + _replayId = replayId; + hub.configureScope((s) { + // ignore: invalid_use_of_internal_member + s.replayId = replayId; + }); + } + + Uint8List? imageBytes; + await _replayRecorder?.capture((image) async { + final imageData = + await image.toByteData(format: ImageByteFormat.png); + if (imageData != null) { + options.logger( + SentryLevel.debug, + 'Replay: captured screenshot (' + '${image.width}x${image.height} pixels, ' + '${imageData.lengthInBytes} bytes)'); + imageBytes = imageData.buffer.asUint8List(); + } else { + options.logger(SentryLevel.warning, + 'Replay: failed to convert screenshot to PNG'); + } + }); + return imageBytes; + default: + throw UnimplementedError('Method ${call.method} not implemented'); + } + }); + } + + return super.init(hub); + } + @override int? startProfiler(SentryId traceId) => tryCatchSync('startProfiler', () { final cSentryId = cocoa.SentryId1.alloc(_lib) diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 1b0ef13cc5..5ccd3a1c67 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -1,10 +1,128 @@ +import 'dart:ui'; + import 'package:meta/meta.dart'; +import '../../../sentry_flutter.dart'; +import '../../event_processor/replay_event_processor.dart'; +import '../../replay/scheduled_recorder.dart'; +import '../../replay/recorder_config.dart'; import '../sentry_native_channel.dart'; // Note: currently this doesn't do anything. Later, it shall be used with // generated JNI bindings. See https://github.com/getsentry/sentry-dart/issues/1444 @internal class SentryNativeJava extends SentryNativeChannel { + ScheduledScreenshotRecorder? _replayRecorder; SentryNativeJava(super.options, super.channel); + + @override + Future init(Hub hub) async { + // We only need these when replay is enabled (session or error capture) + // so let's set it up conditionally. This allows Dart to trim the code. + if (options.experimental.replay.isEnabled) { + // We only need the integration when error-replay capture is enabled. + if ((options.experimental.replay.errorSampleRate ?? 0) > 0) { + options.addEventProcessor(ReplayEventProcessor(this)); + } + + channel.setMethodCallHandler((call) async { + switch (call.method) { + case 'ReplayRecorder.start': + final replayId = + SentryId.fromId(call.arguments['replayId'] as String); + + _startRecorder( + call.arguments['directory'] as String, + ScheduledScreenshotRecorderConfig( + width: call.arguments['width'] as int, + height: call.arguments['height'] as int, + frameRate: call.arguments['frameRate'] as int, + ), + ); + + hub.configureScope((s) { + // ignore: invalid_use_of_internal_member + s.replayId = replayId; + }); + + break; + case 'ReplayRecorder.stop': + await _replayRecorder?.stop(); + _replayRecorder = null; + + hub.configureScope((s) { + // ignore: invalid_use_of_internal_member + s.replayId = null; + }); + + break; + case 'ReplayRecorder.pause': + await _replayRecorder?.stop(); + break; + case 'ReplayRecorder.resume': + _replayRecorder?.start(); + break; + default: + throw UnimplementedError('Method ${call.method} not implemented'); + } + }); + } + + return super.init(hub); + } + + @override + Future close() async { + await _replayRecorder?.stop(); + _replayRecorder = null; + return super.close(); + } + + void _startRecorder( + String cacheDir, ScheduledScreenshotRecorderConfig config) { + // Note: time measurements using a Stopwatch in a debug build: + // save as rawRgba (1230876 bytes): 0.257 ms -- discarded + // save as PNG (25401 bytes): 43.110 ms -- used for the final image + // image size: 25401 bytes + // save to file: 3.677 ms + // onScreenshotRecorded1: 1.237 ms + // released and exiting callback: 0.021 ms + ScreenshotRecorderCallback callback = (image) async { + var imageData = await image.toByteData(format: ImageByteFormat.png); + if (imageData != null) { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final filePath = "$cacheDir/$timestamp.png"; + + options.logger( + SentryLevel.debug, + 'Replay: Saving screenshot to $filePath (' + '${image.width}x${image.height} pixels, ' + '${imageData.lengthInBytes} bytes)'); + try { + await options.fileSystem + .file(filePath) + .writeAsBytes(imageData.buffer.asUint8List(), flush: true); + + await channel.invokeMethod( + 'addReplayScreenshot', + {'path': filePath, 'timestamp': timestamp}, + ); + } catch (error, stackTrace) { + options.logger( + SentryLevel.error, + 'Native call `addReplayScreenshot` failed', + exception: error, + stackTrace: stackTrace, + ); + // ignore: invalid_use_of_internal_member + if (options.automatedTestMode) { + rethrow; + } + } + } + }; + + _replayRecorder = ScheduledScreenshotRecorder(config, callback, options) + ..start(); + } } diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 002790fc32..44ee6432b5 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -10,7 +10,7 @@ import 'native_frames.dart'; /// Provide typed methods to access native layer. @internal abstract class SentryNativeBinding { - Future init(SentryFlutterOptions options); + Future init(Hub hub); Future close(); @@ -57,4 +57,8 @@ abstract class SentryNativeBinding { Future pauseAppHangTracking(); Future resumeAppHangTracking(); + + Future nativeCrash(); + + Future captureReplay(bool isCrash); } diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 623111bb9e..2820ac28ef 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -22,15 +22,15 @@ class SentryNativeChannel @override final SentryFlutterOptions options; - final SentrySafeMethodChannel _channel; + @protected + final SentrySafeMethodChannel channel; SentryNativeChannel(this.options, MethodChannel channel) - : _channel = SentrySafeMethodChannel(channel, options); + : channel = SentrySafeMethodChannel(channel, options); @override - Future init(SentryFlutterOptions options) async { - assert(this.options == options); - return _channel.invokeMethod('initNativeSdk', { + Future init(Hub hub) async { + return channel.invokeMethod('initNativeSdk', { 'dsn': options.dsn, 'debug': options.debug, 'environment': options.environment, @@ -67,37 +67,40 @@ class SentryNativeChannel 'appHangTimeoutIntervalMillis': options.appHangTimeoutInterval.inMilliseconds, if (options.proxy != null) 'proxy': options.proxy?.toJson(), + 'replay': { + 'sessionSampleRate': options.experimental.replay.sessionSampleRate, + 'errorSampleRate': options.experimental.replay.errorSampleRate, + }, }); } @override - Future close() async => _channel.invokeMethod('closeNativeSdk'); + Future close() async => channel.invokeMethod('closeNativeSdk'); @override Future fetchNativeAppStart() async { final json = - await _channel.invokeMapMethod('fetchNativeAppStart'); + await channel.invokeMapMethod('fetchNativeAppStart'); return (json != null) ? NativeAppStart.fromJson(json) : null; } @override Future captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - return _channel.invokeMethod( + return channel.invokeMethod( 'captureEnvelope', [envelopeData, containsUnhandledException]); } @override Future?> loadContexts() => - _channel.invokeMapMethod('loadContexts'); + channel.invokeMapMethod('loadContexts'); @override - Future beginNativeFrames() => - _channel.invokeMethod('beginNativeFrames'); + Future beginNativeFrames() => channel.invokeMethod('beginNativeFrames'); @override Future endNativeFrames(SentryId id) async { - final json = await _channel.invokeMapMethod( + final json = await channel.invokeMapMethod( 'endNativeFrames', {'id': id.toString()}); return (json != null) ? NativeFrames.fromJson(json) : null; } @@ -107,7 +110,7 @@ class SentryNativeChannel final normalizedUser = user?.copyWith( data: MethodChannelHelper.normalizeMap(user.data), ); - await _channel.invokeMethod( + await channel.invokeMethod( 'setUser', {'user': normalizedUser?.toJson()}, ); @@ -118,42 +121,42 @@ class SentryNativeChannel final normalizedBreadcrumb = breadcrumb.copyWith( data: MethodChannelHelper.normalizeMap(breadcrumb.data), ); - await _channel.invokeMethod( + await channel.invokeMethod( 'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()}, ); } @override - Future clearBreadcrumbs() => _channel.invokeMethod('clearBreadcrumbs'); + Future clearBreadcrumbs() => channel.invokeMethod('clearBreadcrumbs'); @override - Future setContexts(String key, dynamic value) => _channel.invokeMethod( + Future setContexts(String key, dynamic value) => channel.invokeMethod( 'setContexts', {'key': key, 'value': MethodChannelHelper.normalize(value)}, ); @override Future removeContexts(String key) => - _channel.invokeMethod('removeContexts', {'key': key}); + channel.invokeMethod('removeContexts', {'key': key}); @override - Future setExtra(String key, dynamic value) => _channel.invokeMethod( + Future setExtra(String key, dynamic value) => channel.invokeMethod( 'setExtra', {'key': key, 'value': MethodChannelHelper.normalize(value)}, ); @override Future removeExtra(String key) => - _channel.invokeMethod('removeExtra', {'key': key}); + channel.invokeMethod('removeExtra', {'key': key}); @override Future setTag(String key, String value) => - _channel.invokeMethod('setTag', {'key': key, 'value': value}); + channel.invokeMethod('setTag', {'key': key, 'value': value}); @override Future removeTag(String key) => - _channel.invokeMethod('removeTag', {'key': key}); + channel.invokeMethod('removeTag', {'key': key}); @override int? startProfiler(SentryId traceId) => @@ -161,12 +164,12 @@ class SentryNativeChannel @override Future discardProfiler(SentryId traceId) => - _channel.invokeMethod('discardProfiler', traceId.toString()); + channel.invokeMethod('discardProfiler', traceId.toString()); @override Future?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs) => - _channel.invokeMapMethod('collectProfile', { + channel.invokeMapMethod('collectProfile', { 'traceId': traceId.toString(), 'startTime': startTimeNs, 'endTime': endTimeNs, @@ -175,7 +178,7 @@ class SentryNativeChannel @override Future?> loadDebugImages() => tryCatchAsync('loadDebugImages', () async { - final images = await _channel + final images = await channel .invokeListMethod>('loadImageList'); return images ?.map((e) => e.cast()) @@ -185,13 +188,22 @@ class SentryNativeChannel @override Future displayRefreshRate() => - _channel.invokeMethod('displayRefreshRate'); + channel.invokeMethod('displayRefreshRate'); @override Future pauseAppHangTracking() => - _channel.invokeMethod('pauseAppHangTracking'); + channel.invokeMethod('pauseAppHangTracking'); @override Future resumeAppHangTracking() => - _channel.invokeMethod('resumeAppHangTracking'); + channel.invokeMethod('resumeAppHangTracking'); + + @override + Future nativeCrash() => channel.invokeMethod('nativeCrash'); + + @override + Future captureReplay(bool isCrash) => + channel.invokeMethod('captureReplay', { + 'isCrash': isCrash, + }).then((value) => SentryId.fromId(value as String)); } diff --git a/flutter/lib/src/native/sentry_safe_method_channel.dart b/flutter/lib/src/native/sentry_safe_method_channel.dart index aa44c08f44..8cd258c8dc 100644 --- a/flutter/lib/src/native/sentry_safe_method_channel.dart +++ b/flutter/lib/src/native/sentry_safe_method_channel.dart @@ -14,6 +14,10 @@ class SentrySafeMethodChannel with SentryNativeSafeInvoker { SentrySafeMethodChannel(this._channel, this.options); + void setMethodCallHandler( + Future Function(MethodCall call)? handler) => + _channel.setMethodCallHandler(handler); + @optionalTypeArgs Future invokeMethod(String method, [dynamic args]) => tryCatchAsync(method, () => _channel.invokeMethod(method, args)); diff --git a/flutter/lib/src/replay/recorder.dart b/flutter/lib/src/replay/recorder.dart new file mode 100644 index 0000000000..a1f4ea1a0b --- /dev/null +++ b/flutter/lib/src/replay/recorder.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; +import 'recorder_config.dart'; +import 'widget_filter.dart'; + +@internal +typedef ScreenshotRecorderCallback = Future Function(Image); + +@internal +class ScreenshotRecorder { + @protected + final ScreenshotRecorderConfig config; + @protected + final SentryFlutterOptions options; + WidgetFilter? _widgetFilter; + bool warningLogged = false; + + ScreenshotRecorder(this.config, this.options) { + final replayOptions = options.experimental.replay; + if (replayOptions.redactAllText || replayOptions.redactAllImages) { + _widgetFilter = WidgetFilter( + redactText: replayOptions.redactAllText, + redactImages: replayOptions.redactAllImages, + logger: options.logger); + } + } + + Future capture(ScreenshotRecorderCallback callback) async { + final context = sentryScreenshotWidgetGlobalKey.currentContext; + final renderObject = context?.findRenderObject() as RenderRepaintBoundary?; + if (context == null || renderObject == null) { + if (!warningLogged) { + options.logger( + SentryLevel.warning, + "Replay: SentryScreenshotWidget is not attached. " + "Skipping replay capture."); + warningLogged = true; + } + return; + } + + try { + final watch = Stopwatch()..start(); + + // On Android, the desired resolution (coming from the configuration) + // is rounded to next multitude of 16 . Therefore, we scale the image. + // On iOS, the screenshot resolution is not adjusted. + final srcWidth = renderObject.size.width; + final srcHeight = renderObject.size.height; + final pixelRatio = config.getPixelRatio(srcWidth, srcHeight); + + // First, we synchronously capture the image and enumerate widgets on the main UI loop. + final futureImage = renderObject.toImage(pixelRatio: pixelRatio); + + final filter = _widgetFilter; + if (filter != null) { + filter.obscure( + context, + pixelRatio, + Rect.fromLTWH(0, 0, srcWidth * pixelRatio, srcHeight * pixelRatio), + ); + } + + final blockingTime = watch.elapsedMilliseconds; + + // Then we draw the image and obscure collected coordinates asynchronously. + final recorder = PictureRecorder(); + final canvas = Canvas(recorder); + final image = await futureImage; + try { + canvas.drawImage(image, Offset.zero, Paint()); + } finally { + image.dispose(); + } + + if (filter != null) { + _obscureWidgets(canvas, filter.items); + } + + final picture = recorder.endRecording(); + + try { + final finalImage = await picture.toImage( + (srcWidth * pixelRatio).round(), (srcHeight * pixelRatio).round()); + try { + await callback(finalImage); + } finally { + finalImage.dispose(); + } + } finally { + picture.dispose(); + } + + options.logger( + SentryLevel.debug, + "Replay: captured a screenshot in ${watch.elapsedMilliseconds}" + " ms ($blockingTime ms blocking)."); + } catch (e, stackTrace) { + options.logger(SentryLevel.error, "Replay: failed to capture screenshot.", + exception: e, stackTrace: stackTrace); + // ignore: invalid_use_of_internal_member + if (options.automatedTestMode) { + rethrow; + } + } + } + + void _obscureWidgets(Canvas canvas, List items) { + final paint = Paint()..style = PaintingStyle.fill; + for (var item in items) { + paint.color = item.color; + canvas.drawRect(item.bounds, paint); + } + } +} diff --git a/flutter/lib/src/replay/recorder_config.dart b/flutter/lib/src/replay/recorder_config.dart new file mode 100644 index 0000000000..9649a33823 --- /dev/null +++ b/flutter/lib/src/replay/recorder_config.dart @@ -0,0 +1,29 @@ +import 'dart:math'; + +import 'package:meta/meta.dart'; + +@internal +class ScreenshotRecorderConfig { + final int? width; + final int? height; + + const ScreenshotRecorderConfig({this.width, this.height}); + + double getPixelRatio(double srcWidth, double srcHeight) { + assert((width == null) == (height == null)); + if (width == null || height == null) { + return 1.0; + } + return min(width! / srcWidth, height! / srcHeight); + } +} + +class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { + final int frameRate; + + const ScheduledScreenshotRecorderConfig({ + super.width, + super.height, + required this.frameRate, + }); +} diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart new file mode 100644 index 0000000000..c575278a74 --- /dev/null +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -0,0 +1,40 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; +import 'recorder.dart'; +import 'recorder_config.dart'; +import 'scheduler.dart'; + +@internal +typedef ScreenshotRecorderCallback = Future Function(Image); + +@internal +class ScheduledScreenshotRecorder extends ScreenshotRecorder { + late final Scheduler _scheduler; + final ScreenshotRecorderCallback _callback; + + ScheduledScreenshotRecorder(ScheduledScreenshotRecorderConfig config, + this._callback, SentryFlutterOptions options) + : super(config, options) { + assert(config.frameRate > 0); + final frameDuration = Duration(milliseconds: 1000 ~/ config.frameRate); + _scheduler = Scheduler(frameDuration, _capture, + options.bindingUtils.instance!.addPostFrameCallback); + } + + void start() { + options.logger(SentryLevel.debug, "Replay: starting replay capture."); + _scheduler.start(); + } + + Future stop() async { + await _scheduler.stop(); + options.logger(SentryLevel.debug, "Replay: replay capture stopped."); + } + + Future _capture(Duration sinceSchedulerEpoch) async => + capture(_callback); +} diff --git a/flutter/lib/src/replay/scheduler.dart b/flutter/lib/src/replay/scheduler.dart new file mode 100644 index 0000000000..4d246360e3 --- /dev/null +++ b/flutter/lib/src/replay/scheduler.dart @@ -0,0 +1,55 @@ +import 'package:flutter/scheduler.dart'; +import 'package:meta/meta.dart'; + +@internal +typedef SchedulerCallback = Future Function(Duration); + +/// This is a low-priority scheduler. +/// We're not using Timer.periodic() because it may schedule a callback +/// even if the previous call hasn't finished (or started) yet. +/// Instead, we manually schedule a callback with a given delay after the +/// previous callback finished. Therefore, if the capture takes too long, we +/// won't overload the system. We sacrifice the frame rate for performance. +@internal +class Scheduler { + final SchedulerCallback _callback; + final Duration _interval; + bool _running = false; + Future? _scheduled; + + final void Function(FrameCallback callback) _addPostFrameCallback; + + Scheduler(this._interval, this._callback, this._addPostFrameCallback); + + void start() { + _running = true; + if (_scheduled == null) { + _runAfterNextFrame(); + } + } + + Future stop() async { + _running = false; + final scheduled = _scheduled; + _scheduled = null; + if (scheduled != null) { + await scheduled; + } + } + + @pragma('vm:prefer-inline') + void _scheduleNext() { + _scheduled ??= Future.delayed(_interval, _runAfterNextFrame); + } + + @pragma('vm:prefer-inline') + void _runAfterNextFrame() { + _scheduled = null; + _addPostFrameCallback(_run); + } + + void _run(Duration sinceSchedulerEpoch) { + if (!_running) return; + _callback(sinceSchedulerEpoch).then((_) => _scheduleNext()); + } +} diff --git a/flutter/lib/src/replay/widget_filter.dart b/flutter/lib/src/replay/widget_filter.dart new file mode 100644 index 0000000000..83e069cb97 --- /dev/null +++ b/flutter/lib/src/replay/widget_filter.dart @@ -0,0 +1,133 @@ +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; + +import '../../sentry_flutter.dart'; + +@internal +class WidgetFilter { + final items = []; + final SentryLogger logger; + final bool redactText; + final bool redactImages; + static const _defaultColor = Color.fromARGB(255, 0, 0, 0); + late double _pixelRatio; + late Rect _bounds; + final _warnedWidgets = {}; + + WidgetFilter( + {required this.redactText, + required this.redactImages, + required this.logger}); + + void obscure(BuildContext context, double pixelRatio, Rect bounds) { + _pixelRatio = pixelRatio; + _bounds = bounds; + items.clear(); + if (context is Element) { + _obscure(context); + } else { + context.visitChildElements(_obscure); + } + } + + void _obscure(Element element) { + final widget = element.widget; + + if (!_isVisible(widget)) { + assert(() { + logger(SentryLevel.debug, "WidgetFilter skipping invisible: $widget"); + return true; + }()); + return; + } + + final obscured = _obscureIfNeeded(element, widget); + if (!obscured) { + element.visitChildElements(_obscure); + } + } + + @pragma('vm:prefer-inline') + bool _obscureIfNeeded(Element element, Widget widget) { + Color? color; + + if (redactText && widget is Text) { + color = widget.style?.color; + } else if (redactText && widget is EditableText) { + color = widget.style.color; + } else if (redactImages && widget is Image) { + color = widget.color; + } else { + // No other type is currently obscured. + return false; + } + + final renderObject = element.renderObject; + if (renderObject is! RenderBox) { + _cantObscure(widget, "it's renderObject is not a RenderBox"); + return false; + } + + final size = element.size; + if (size == null) { + _cantObscure(widget, "it's renderObject has a null size"); + return false; + } + + final offset = renderObject.localToGlobal(Offset.zero); + + final rect = Rect.fromLTWH( + offset.dx * _pixelRatio, + offset.dy * _pixelRatio, + size.width * _pixelRatio, + size.height * _pixelRatio, + ); + + if (!rect.overlaps(_bounds)) { + assert(() { + logger(SentryLevel.debug, "WidgetFilter skipping offscreen: $widget"); + return true; + }()); + return false; + } + + items.add(WidgetFilterItem(color ?? _defaultColor, rect)); + assert(() { + logger(SentryLevel.debug, "WidgetFilter obscuring: $widget"); + return true; + }()); + + return true; + } + + // We cut off some widgets early because they're not visible at all. + bool _isVisible(Widget widget) { + if (widget is Visibility) { + return widget.visible; + } + if (widget is Opacity) { + return widget.opacity > 0; + } + if (widget is Offstage) { + return !widget.offstage; + } + return true; + } + + @pragma('vm:prefer-inline') + void _cantObscure(Widget widget, String message) { + if (!_warnedWidgets.contains(widget.hashCode)) { + _warnedWidgets.add(widget.hashCode); + logger(SentryLevel.warning, + "WidgetFilter cannot obscure widget $widget: $message"); + } + } +} + +class WidgetFilterItem { + final Color color; + final Rect bounds; + + const WidgetFilterItem(this.color, this.bounds); +} diff --git a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart index e83d46d0c5..6eafb935a5 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_widget.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_widget.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; -import '../../sentry_flutter.dart'; - /// Key which is used to identify the [RepaintBoundary] @internal final sentryScreenshotWidgetGlobalKey = @@ -25,36 +23,19 @@ final sentryScreenshotWidgetGlobalKey = /// times. class SentryScreenshotWidget extends StatefulWidget { final Widget child; - late final Hub _hub; - - SentryFlutterOptions? get _options => - // ignore: invalid_use_of_internal_member - _hub.options is SentryFlutterOptions - // ignore: invalid_use_of_internal_member - ? _hub.options as SentryFlutterOptions - : null; - SentryScreenshotWidget({ - super.key, - required this.child, - @internal Hub? hub, - }) : _hub = hub ?? HubAdapter(); + const SentryScreenshotWidget({super.key, required this.child}); @override _SentryScreenshotWidgetState createState() => _SentryScreenshotWidgetState(); } class _SentryScreenshotWidgetState extends State { - SentryFlutterOptions? get _options => widget._options; - @override Widget build(BuildContext context) { - if (_options?.attachScreenshot ?? false) { - return RepaintBoundary( - key: sentryScreenshotWidgetGlobalKey, - child: widget.child, - ); - } - return widget.child; + return RepaintBoundary( + key: sentryScreenshotWidgetGlobalKey, + child: widget.child, + ); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 5472a87bf7..29f533d082 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -251,11 +251,7 @@ mixin SentryFlutter { /// Only for iOS and macOS. static Future pauseAppHangTracking() { if (_native == null) { - // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( - SentryLevel.debug, - 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the pauseAppHangTracking API.', - ); + _logNativeIntegrationNotAvailable("pauseAppHangTracking"); return Future.value(); } return _native!.pauseAppHangTracking(); @@ -265,11 +261,7 @@ mixin SentryFlutter { /// Only for iOS and macOS static Future resumeAppHangTracking() { if (_native == null) { - // ignore: invalid_use_of_internal_member - Sentry.currentHub.options.logger( - SentryLevel.debug, - 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the resumeAppHangTracking API.', - ); + _logNativeIntegrationNotAvailable("resumeAppHangTracking"); return Future.value(); } return _native!.resumeAppHangTracking(); @@ -282,4 +274,23 @@ mixin SentryFlutter { static set native(SentryNativeBinding? value) => _native = value; static SentryNativeBinding? _native; + + /// Use `nativeCrash()` to crash the native implementation and test/debug the crash reporting for native code. + /// This should not be used in production code. + /// Only for Android, iOS and macOS + static Future nativeCrash() { + if (_native == null) { + _logNativeIntegrationNotAvailable("nativeCrash"); + return Future.value(); + } + return _native!.nativeCrash(); + } + + static void _logNativeIntegrationNotAvailable(String methodName) { + // ignore: invalid_use_of_internal_member + Sentry.currentHub.options.logger( + SentryLevel.debug, + 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the $methodName API.', + ); + } } diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index facb5d0d02..308ed805b0 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -1,6 +1,8 @@ import 'dart:async'; -import 'package:meta/meta.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:meta/meta.dart' as meta; import 'package:sentry/sentry.dart'; import 'package:flutter/widgets.dart'; @@ -10,6 +12,7 @@ import 'screenshot/sentry_screenshot_quality.dart'; import 'event_processor/screenshot_event_processor.dart'; import 'screenshot/sentry_screenshot_widget.dart'; import 'sentry_flutter.dart'; +import 'sentry_replay_options.dart'; import 'user_interaction/sentry_user_interaction_widget.dart'; /// This class adds options which are only available in a Flutter environment. @@ -218,14 +221,14 @@ class SentryFlutterOptions extends SentryOptions { /// Sets the Proguard uuid for Android platform. String? proguardUuid; - @internal + @meta.internal late RendererWrapper rendererWrapper = RendererWrapper(); /// Enables the View Hierarchy feature. /// /// Renders an ASCII represention of the entire view hierarchy of the /// application when an error happens and includes it as an attachment. - @experimental + @meta.experimental bool attachViewHierarchy = false; /// Enables collection of view hierarchy element identifiers. @@ -317,14 +320,14 @@ class SentryFlutterOptions extends SentryOptions { } /// Setting this to a custom [BindingWrapper] allows you to use a custom [WidgetsBinding]. - @experimental + @meta.experimental BindingWrapper bindingUtils = BindingWrapper(); /// The sample rate for profiling traces in the range of 0.0 to 1.0. /// This is relative to tracesSampleRate - it is a ratio of profiled traces out of all sampled traces. /// At the moment, only apps targeting iOS and macOS are supported. @override - @experimental + @meta.experimental double? get profilesSampleRate { // ignore: invalid_use_of_internal_member return super.profilesSampleRate; @@ -334,7 +337,7 @@ class SentryFlutterOptions extends SentryOptions { /// This is relative to tracesSampleRate - it is a ratio of profiled traces out of all sampled traces. /// At the moment, only apps targeting iOS and macOS are supported. @override - @experimental + @meta.experimental set profilesSampleRate(double? value) { // ignore: invalid_use_of_internal_member super.profilesSampleRate = value; @@ -342,6 +345,20 @@ class SentryFlutterOptions extends SentryOptions { /// The [navigatorKey] is used to add information of the currently used locale to the contexts. GlobalKey? navigatorKey; + + @meta.internal + FileSystem fileSystem = LocalFileSystem(); + + /// Configuration of experimental features that may change or be removed + /// without prior notice. Additionally, these features may not be ready for + /// production use yet. + @meta.experimental + final experimental = _SentryFlutterExperimentalOptions(); +} + +class _SentryFlutterExperimentalOptions { + /// Replay recording configuration. + final replay = SentryReplayOptions(); } /// Callback being executed in [ScreenshotEventProcessor], deciding if a diff --git a/flutter/lib/src/sentry_replay_options.dart b/flutter/lib/src/sentry_replay_options.dart new file mode 100644 index 0000000000..e98aed7418 --- /dev/null +++ b/flutter/lib/src/sentry_replay_options.dart @@ -0,0 +1,40 @@ +import 'package:meta/meta.dart'; + +/// Configuration of the experimental replay feature. +class SentryReplayOptions { + double? _sessionSampleRate; + + /// A percentage of sessions in which a replay will be created. + /// The value needs to be >= 0.0 and <= 1.0. + /// Specifying 0 means none, 1.0 means 100 %. Defaults to null (disabled). + double? get sessionSampleRate => _sessionSampleRate; + set sessionSampleRate(double? value) { + assert(value == null || (value >= 0 && value <= 1)); + _sessionSampleRate = value; + } + + double? _errorSampleRate; + + /// A percentage of errors that will be accompanied by a 30 seconds replay. + /// The value needs to be >= 0.0 and <= 1.0. + /// Specifying 0 means none, 1.0 means 100 %. Defaults to null (disabled). + double? get errorSampleRate => _errorSampleRate; + set errorSampleRate(double? value) { + assert(value == null || (value >= 0 && value <= 1)); + _errorSampleRate = value; + } + + /// Redact all text content. Draws a rectangle of text bounds with text color + /// on top. Currently, only [Text] and [EditableText] Widgets are redacted. + /// Default is enabled. + var redactAllText = true; + + /// Redact all image content. Draws a rectangle of image bounds with image's + /// dominant color on top. Currently, only [Image] widgets are redacted. + /// Default is enabled. + var redactAllImages = true; + + @internal + bool get isEnabled => + ((sessionSampleRate ?? 0) > 0) || ((errorSampleRate ?? 0) > 0); +} diff --git a/flutter/lib/src/version.dart b/flutter/lib/src/version.dart index fec35b52f3..f17144accf 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.7.0'; +const String sdkVersion = '8.8.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 d71721230d..ad36c1252f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry_flutter -version: 8.7.0 +version: 8.8.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,10 +23,11 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - sentry: 8.7.0 + sentry: 8.8.0 package_info_plus: '>=1.0.0' meta: ^1.3.0 ffi: ^2.0.0 + file: '>=6.1.4' dev_dependencies: build_runner: ^2.4.2 @@ -56,4 +57,4 @@ flutter: linux: pluginClass: SentryFlutterPlugin windows: - pluginClass: SentryFlutterPlugin + ffiPlugin: true diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index 3a00f10ced..819e3b9b7b 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -34,7 +34,6 @@ void main() { final sut = fixture.getSut(renderer, isWeb); await tester.pumpWidget(SentryScreenshotWidget( - hub: fixture.hub, child: Text('Catching Pokémon is a snap!', textDirection: TextDirection.ltr))); diff --git a/flutter/test/integrations/init_native_sdk_test.dart b/flutter/test/integrations/init_native_sdk_test.dart index d3faa93346..4b89d3cfe0 100644 --- a/flutter/test/integrations/init_native_sdk_test.dart +++ b/flutter/test/integrations/init_native_sdk_test.dart @@ -8,6 +8,7 @@ import 'package:sentry_flutter/src/native/sentry_native_channel.dart'; import 'package:sentry_flutter/src/version.dart'; import '../mocks.dart'; +import '../mocks.mocks.dart'; void main() { late Fixture fixture; @@ -25,7 +26,7 @@ void main() { }); var sut = fixture.getSut(channel); - await sut.init(fixture.options); + await sut.init(MockHub()); channel.setMethodCallHandler(null); @@ -64,6 +65,10 @@ void main() { 'connectionTimeoutMillis': 5000, 'readTimeoutMillis': 5000, 'appHangTimeoutIntervalMillis': 2000, + 'replay': { + 'sessionSampleRate': null, + 'errorSampleRate': null, + }, }); }); @@ -111,12 +116,14 @@ void main() { type: SentryProxyType.http, user: 'admin', pass: '0000', - ); + ) + ..experimental.replay.sessionSampleRate = 0.1 + ..experimental.replay.errorSampleRate = 0.2; fixture.options.sdk.addIntegration('foo'); fixture.options.sdk.addPackage('bar', '1'); - await sut.init(fixture.options); + await sut.init(MockHub()); channel.setMethodCallHandler(null); @@ -162,7 +169,11 @@ void main() { 'type': 'HTTP', 'user': 'admin', 'pass': '0000', - } + }, + 'replay': { + 'sessionSampleRate': 0.1, + 'errorSampleRate': 0.2, + }, }); }); } diff --git a/flutter/test/integrations/native_sdk_integration_test.dart b/flutter/test/integrations/native_sdk_integration_test.dart index f94f88698c..5c244b3d66 100644 --- a/flutter/test/integrations/native_sdk_integration_test.dart +++ b/flutter/test/integrations/native_sdk_integration_test.dart @@ -60,7 +60,7 @@ void main() { class _ThrowingMockSentryNative extends MockSentryNativeBinding { @override - Future init(SentryFlutterOptions? options) async { + Future init(Hub? hub) async { throw Exception(); } } diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index f5e5cb65ce..ee74889877 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -54,46 +54,24 @@ ISentrySpan startTransactionShim( void main() {} class MockPlatform with NoSuchMethodProvider implements Platform { - MockPlatform({ - String? os, - String? osVersion, - String? hostname, - }) : operatingSystem = os ?? '', - operatingSystemVersion = osVersion ?? '', - localHostname = hostname ?? ''; - - factory MockPlatform.android() { - return MockPlatform(os: 'android'); - } + const MockPlatform(this.operatingSystem, + {this.operatingSystemVersion = '', this.localHostname = ''}); - factory MockPlatform.iOs() { - return MockPlatform(os: 'ios'); - } - - factory MockPlatform.macOs() { - return MockPlatform(os: 'macos'); - } - - factory MockPlatform.windows() { - return MockPlatform(os: 'windows'); - } - - factory MockPlatform.linux() { - return MockPlatform(os: 'linux'); - } - - factory MockPlatform.fuchsia() { - return MockPlatform(os: 'fuchsia'); - } + const MockPlatform.android() : this('android'); + const MockPlatform.iOs() : this('ios'); + const MockPlatform.macOs() : this('macos'); + const MockPlatform.windows() : this('windows'); + const MockPlatform.linux() : this('linux'); + const MockPlatform.fuchsia() : this('fuchsia'); @override - String operatingSystem; + final String operatingSystem; @override - String operatingSystemVersion; + final String operatingSystemVersion; @override - String localHostname; + final String localHostname; @override bool get isLinux => (operatingSystem == 'linux'); @@ -122,7 +100,7 @@ class MockPlatformChecker with NoSuchMethodProvider implements PlatformChecker { this.isWebValue = false, this.hasNativeIntegration = false, Platform? mockPlatform, - }) : _mockPlatform = mockPlatform ?? MockPlatform(); + }) : _mockPlatform = mockPlatform ?? MockPlatform(''); final bool isDebug; final bool isProfile; @@ -206,3 +184,32 @@ final fakeFrameDurations = [ Duration(milliseconds: 40), Duration(milliseconds: 710), ]; + +@GenerateMocks([Callbacks]) +abstract class Callbacks { + Future? methodCallHandler(String method, [dynamic arguments]); +} + +class NativeChannelFixture { + late final MethodChannel channel; + late final Future? Function(String method, [dynamic arguments]) + handler; + static TestDefaultBinaryMessenger get _messenger => + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + NativeChannelFixture() { + TestWidgetsFlutterBinding.ensureInitialized(); + channel = MethodChannel('test.channel', StandardMethodCodec(), _messenger); + handler = MockCallbacks().methodCallHandler; + _messenger.setMockMethodCallHandler( + channel, (call) => handler(call.method, call.arguments)); + } + + // Mock this call as if it was invoked by the native side. + Future invokeFromNative(String method, [dynamic arguments]) async { + final call = + StandardMethodCodec().encodeMethodCall(MethodCall(method, arguments)); + return _messenger.handlePlatformMessage( + channel.name, call, (ByteData? data) {}); + } +} diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 01d2127efe..feb97b5927 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -3,27 +3,22 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i8; -import 'dart:typed_data' as _i16; +import 'dart:async' as _i7; +import 'dart:typed_data' as _i12; -import 'package:flutter/src/services/binary_messenger.dart' as _i6; -import 'package:flutter/src/services/message_codec.dart' as _i5; -import 'package:flutter/src/services/platform_channel.dart' as _i12; +import 'package:flutter/services.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i10; -import 'package:sentry/sentry.dart' as _i2; -import 'package:sentry/src/metrics/metric.dart' as _i19; -import 'package:sentry/src/metrics/metrics_api.dart' as _i7; -import 'package:sentry/src/profiling.dart' as _i11; -import 'package:sentry/src/protocol.dart' as _i3; -import 'package:sentry/src/sentry_envelope.dart' as _i9; -import 'package:sentry/src/sentry_tracer.dart' as _i4; -import 'package:sentry_flutter/sentry_flutter.dart' as _i14; -import 'package:sentry_flutter/src/native/native_app_start.dart' as _i15; -import 'package:sentry_flutter/src/native/native_frames.dart' as _i17; -import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i13; - -import 'mocks.dart' as _i18; +import 'package:mockito/src/dummies.dart' as _i8; +import 'package:sentry/src/metrics/metric.dart' as _i14; +import 'package:sentry/src/metrics/metrics_api.dart' as _i5; +import 'package:sentry/src/profiling.dart' as _i9; +import 'package:sentry/src/sentry_tracer.dart' as _i3; +import 'package:sentry_flutter/sentry_flutter.dart' as _i2; +import 'package:sentry_flutter/src/native/native_app_start.dart' as _i11; +import 'package:sentry_flutter/src/native/native_frames.dart' as _i13; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i10; + +import 'mocks.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -70,7 +65,7 @@ class _FakeISentrySpan_2 extends _i1.SmartFake implements _i2.ISentrySpan { } class _FakeSentryTraceHeader_3 extends _i1.SmartFake - implements _i3.SentryTraceHeader { + implements _i2.SentryTraceHeader { _FakeSentryTraceHeader_3( Object parent, Invocation parentInvocation, @@ -80,7 +75,7 @@ class _FakeSentryTraceHeader_3 extends _i1.SmartFake ); } -class _FakeSentryTracer_4 extends _i1.SmartFake implements _i4.SentryTracer { +class _FakeSentryTracer_4 extends _i1.SmartFake implements _i3.SentryTracer { _FakeSentryTracer_4( Object parent, Invocation parentInvocation, @@ -90,7 +85,7 @@ class _FakeSentryTracer_4 extends _i1.SmartFake implements _i4.SentryTracer { ); } -class _FakeSentryId_5 extends _i1.SmartFake implements _i3.SentryId { +class _FakeSentryId_5 extends _i1.SmartFake implements _i2.SentryId { _FakeSentryId_5( Object parent, Invocation parentInvocation, @@ -100,7 +95,7 @@ class _FakeSentryId_5 extends _i1.SmartFake implements _i3.SentryId { ); } -class _FakeContexts_6 extends _i1.SmartFake implements _i3.Contexts { +class _FakeContexts_6 extends _i1.SmartFake implements _i2.Contexts { _FakeContexts_6( Object parent, Invocation parentInvocation, @@ -111,7 +106,7 @@ class _FakeContexts_6 extends _i1.SmartFake implements _i3.Contexts { } class _FakeSentryTransaction_7 extends _i1.SmartFake - implements _i3.SentryTransaction { + implements _i2.SentryTransaction { _FakeSentryTransaction_7( Object parent, Invocation parentInvocation, @@ -121,7 +116,7 @@ class _FakeSentryTransaction_7 extends _i1.SmartFake ); } -class _FakeMethodCodec_8 extends _i1.SmartFake implements _i5.MethodCodec { +class _FakeMethodCodec_8 extends _i1.SmartFake implements _i4.MethodCodec { _FakeMethodCodec_8( Object parent, Invocation parentInvocation, @@ -132,7 +127,7 @@ class _FakeMethodCodec_8 extends _i1.SmartFake implements _i5.MethodCodec { } class _FakeBinaryMessenger_9 extends _i1.SmartFake - implements _i6.BinaryMessenger { + implements _i4.BinaryMessenger { _FakeBinaryMessenger_9( Object parent, Invocation parentInvocation, @@ -152,7 +147,7 @@ class _FakeSentryOptions_10 extends _i1.SmartFake implements _i2.SentryOptions { ); } -class _FakeMetricsApi_11 extends _i1.SmartFake implements _i7.MetricsApi { +class _FakeMetricsApi_11 extends _i1.SmartFake implements _i5.MetricsApi { _FakeMetricsApi_11( Object parent, Invocation parentInvocation, @@ -182,6 +177,28 @@ class _FakeHub_13 extends _i1.SmartFake implements _i2.Hub { ); } +/// A class which mocks [Callbacks]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCallbacks extends _i1.Mock implements _i6.Callbacks { + MockCallbacks() { + _i1.throwOnMissingStub(this); + } + + @override + _i7.Future? methodCallHandler( + String? method, [ + dynamic arguments, + ]) => + (super.noSuchMethod(Invocation.method( + #methodCallHandler, + [ + method, + arguments, + ], + )) as _i7.Future?); +} + /// A class which mocks [Transport]. /// /// See the documentation for Mockito's code generation for more information. @@ -191,20 +208,20 @@ class MockTransport extends _i1.Mock implements _i2.Transport { } @override - _i8.Future<_i3.SentryId?> send(_i9.SentryEnvelope? envelope) => + _i7.Future<_i2.SentryId?> send(_i2.SentryEnvelope? envelope) => (super.noSuchMethod( Invocation.method( #send, [envelope], ), - returnValue: _i8.Future<_i3.SentryId?>.value(), - ) as _i8.Future<_i3.SentryId?>); + returnValue: _i7.Future<_i2.SentryId?>.value(), + ) as _i7.Future<_i2.SentryId?>); } /// A class which mocks [SentryTracer]. /// /// See the documentation for Mockito's code generation for more information. -class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { +class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { MockSentryTracer() { _i1.throwOnMissingStub(this); } @@ -212,7 +229,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i10.dummyValue( + returnValue: _i8.dummyValue( this, Invocation.getter(#name), ), @@ -228,15 +245,15 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - _i3.SentryTransactionNameSource get transactionNameSource => + _i2.SentryTransactionNameSource get transactionNameSource => (super.noSuchMethod( Invocation.getter(#transactionNameSource), - returnValue: _i3.SentryTransactionNameSource.custom, - ) as _i3.SentryTransactionNameSource); + returnValue: _i2.SentryTransactionNameSource.custom, + ) as _i2.SentryTransactionNameSource); @override set transactionNameSource( - _i3.SentryTransactionNameSource? _transactionNameSource) => + _i2.SentryTransactionNameSource? _transactionNameSource) => super.noSuchMethod( Invocation.setter( #transactionNameSource, @@ -246,7 +263,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profiler(_i11.SentryProfiler? _profiler) => super.noSuchMethod( + set profiler(_i9.SentryProfiler? _profiler) => super.noSuchMethod( Invocation.setter( #profiler, _profiler, @@ -255,7 +272,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profileInfo(_i11.SentryProfileInfo? _profileInfo) => super.noSuchMethod( + set profileInfo(_i9.SentryProfileInfo? _profileInfo) => super.noSuchMethod( Invocation.setter( #profileInfo, _profileInfo, @@ -303,10 +320,10 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ) as bool); @override - List<_i3.SentrySpan> get children => (super.noSuchMethod( + List<_i2.SentrySpan> get children => (super.noSuchMethod( Invocation.getter(#children), - returnValue: <_i3.SentrySpan>[], - ) as List<_i3.SentrySpan>); + returnValue: <_i2.SentrySpan>[], + ) as List<_i2.SentrySpan>); @override set throwable(dynamic throwable) => super.noSuchMethod( @@ -318,7 +335,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set status(_i3.SpanStatus? status) => super.noSuchMethod( + set status(_i2.SpanStatus? status) => super.noSuchMethod( Invocation.setter( #status, status, @@ -339,8 +356,8 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ) as Map); @override - _i8.Future finish({ - _i3.SpanStatus? status, + _i7.Future finish({ + _i2.SpanStatus? status, DateTime? endTimestamp, }) => (super.noSuchMethod( @@ -352,9 +369,9 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { #endTimestamp: endTimestamp, }, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -436,7 +453,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { @override _i2.ISentrySpan startChildWithParentSpanId( - _i3.SpanId? parentSpanId, + _i2.SpanId? parentSpanId, String? operation, { String? description, DateTime? startTimestamp, @@ -470,7 +487,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ) as _i2.ISentrySpan); @override - _i3.SentryTraceHeader toSentryTrace() => (super.noSuchMethod( + _i2.SentryTraceHeader toSentryTrace() => (super.noSuchMethod( Invocation.method( #toSentryTrace, [], @@ -482,7 +499,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { [], ), ), - ) as _i3.SentryTraceHeader); + ) as _i2.SentryTraceHeader); @override void setMeasurement( @@ -516,7 +533,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { /// /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable -class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { +class MockSentryTransaction extends _i1.Mock implements _i2.SentryTransaction { MockSentryTransaction() { _i1.throwOnMissingStub(this); } @@ -540,13 +557,13 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ); @override - List<_i3.SentrySpan> get spans => (super.noSuchMethod( + List<_i2.SentrySpan> get spans => (super.noSuchMethod( Invocation.getter(#spans), - returnValue: <_i3.SentrySpan>[], - ) as List<_i3.SentrySpan>); + returnValue: <_i2.SentrySpan>[], + ) as List<_i2.SentrySpan>); @override - set spans(List<_i3.SentrySpan>? _spans) => super.noSuchMethod( + set spans(List<_i2.SentrySpan>? _spans) => super.noSuchMethod( Invocation.setter( #spans, _spans, @@ -555,13 +572,13 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ); @override - _i4.SentryTracer get tracer => (super.noSuchMethod( + _i3.SentryTracer get tracer => (super.noSuchMethod( Invocation.getter(#tracer), returnValue: _FakeSentryTracer_4( this, Invocation.getter(#tracer), ), - ) as _i4.SentryTracer); + ) as _i3.SentryTracer); @override Map get measurements => (super.noSuchMethod( @@ -580,7 +597,7 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ); @override - set metricSummaries(Map>? _metricSummaries) => + set metricSummaries(Map>? _metricSummaries) => super.noSuchMethod( Invocation.setter( #metricSummaries, @@ -590,7 +607,7 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ); @override - set transactionInfo(_i3.SentryTransactionInfo? _transactionInfo) => + set transactionInfo(_i2.SentryTransactionInfo? _transactionInfo) => super.noSuchMethod( Invocation.setter( #transactionInfo, @@ -612,22 +629,22 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ) as bool); @override - _i3.SentryId get eventId => (super.noSuchMethod( + _i2.SentryId get eventId => (super.noSuchMethod( Invocation.getter(#eventId), returnValue: _FakeSentryId_5( this, Invocation.getter(#eventId), ), - ) as _i3.SentryId); + ) as _i2.SentryId); @override - _i3.Contexts get contexts => (super.noSuchMethod( + _i2.Contexts get contexts => (super.noSuchMethod( Invocation.getter(#contexts), returnValue: _FakeContexts_6( this, Invocation.getter(#contexts), ), - ) as _i3.Contexts); + ) as _i2.Contexts); @override Map toJson() => (super.noSuchMethod( @@ -639,8 +656,8 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ) as Map); @override - _i3.SentryTransaction copyWith({ - _i3.SentryId? eventId, + _i2.SentryTransaction copyWith({ + _i2.SentryId? eventId, DateTime? timestamp, String? platform, String? logger, @@ -649,26 +666,26 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { String? dist, String? environment, Map? modules, - _i3.SentryMessage? message, + _i2.SentryMessage? message, String? transaction, dynamic throwable, - _i3.SentryLevel? level, + _i2.SentryLevel? level, String? culprit, Map? tags, Map? extra, List? fingerprint, - _i3.SentryUser? user, - _i3.Contexts? contexts, - List<_i3.Breadcrumb>? breadcrumbs, - _i3.SdkVersion? sdk, - _i3.SentryRequest? request, - _i3.DebugMeta? debugMeta, - List<_i3.SentryException>? exceptions, - List<_i3.SentryThread>? threads, + _i2.SentryUser? user, + _i2.Contexts? contexts, + List<_i2.Breadcrumb>? breadcrumbs, + _i2.SdkVersion? sdk, + _i2.SentryRequest? request, + _i2.DebugMeta? debugMeta, + List<_i2.SentryException>? exceptions, + List<_i2.SentryThread>? threads, String? type, Map? measurements, - Map>? metricSummaries, - _i3.SentryTransactionInfo? transactionInfo, + Map>? metricSummaries, + _i2.SentryTransactionInfo? transactionInfo, }) => (super.noSuchMethod( Invocation.method( @@ -744,13 +761,13 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { }, ), ), - ) as _i3.SentryTransaction); + ) as _i2.SentryTransaction); } /// A class which mocks [SentrySpan]. /// /// See the documentation for Mockito's code generation for more information. -class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { +class MockSentrySpan extends _i1.Mock implements _i2.SentrySpan { MockSentrySpan() { _i1.throwOnMissingStub(this); } @@ -762,16 +779,16 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { ) as bool); @override - _i4.SentryTracer get tracer => (super.noSuchMethod( + _i3.SentryTracer get tracer => (super.noSuchMethod( Invocation.getter(#tracer), returnValue: _FakeSentryTracer_4( this, Invocation.getter(#tracer), ), - ) as _i4.SentryTracer); + ) as _i3.SentryTracer); @override - set status(_i3.SpanStatus? status) => super.noSuchMethod( + set status(_i2.SpanStatus? status) => super.noSuchMethod( Invocation.setter( #status, status, @@ -834,8 +851,8 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { ) as Map); @override - _i8.Future finish({ - _i3.SpanStatus? status, + _i7.Future finish({ + _i2.SpanStatus? status, DateTime? endTimestamp, }) => (super.noSuchMethod( @@ -847,9 +864,9 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { #endTimestamp: endTimestamp, }, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -939,7 +956,7 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { ) as Map); @override - _i3.SentryTraceHeader toSentryTrace() => (super.noSuchMethod( + _i2.SentryTraceHeader toSentryTrace() => (super.noSuchMethod( Invocation.method( #toSentryTrace, [], @@ -951,7 +968,7 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { [], ), ), - ) as _i3.SentryTraceHeader); + ) as _i2.SentryTraceHeader); @override void setMeasurement( @@ -984,7 +1001,7 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { /// A class which mocks [MethodChannel]. /// /// See the documentation for Mockito's code generation for more information. -class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { +class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel { MockMethodChannel() { _i1.throwOnMissingStub(this); } @@ -992,32 +1009,32 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i10.dummyValue( + returnValue: _i8.dummyValue( this, Invocation.getter(#name), ), ) as String); @override - _i5.MethodCodec get codec => (super.noSuchMethod( + _i4.MethodCodec get codec => (super.noSuchMethod( Invocation.getter(#codec), returnValue: _FakeMethodCodec_8( this, Invocation.getter(#codec), ), - ) as _i5.MethodCodec); + ) as _i4.MethodCodec); @override - _i6.BinaryMessenger get binaryMessenger => (super.noSuchMethod( + _i4.BinaryMessenger get binaryMessenger => (super.noSuchMethod( Invocation.getter(#binaryMessenger), returnValue: _FakeBinaryMessenger_9( this, Invocation.getter(#binaryMessenger), ), - ) as _i6.BinaryMessenger); + ) as _i4.BinaryMessenger); @override - _i8.Future invokeMethod( + _i7.Future invokeMethod( String? method, [ dynamic arguments, ]) => @@ -1029,11 +1046,11 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future?> invokeListMethod( + _i7.Future?> invokeListMethod( String? method, [ dynamic arguments, ]) => @@ -1045,11 +1062,11 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override - _i8.Future?> invokeMapMethod( + _i7.Future?> invokeMapMethod( String? method, [ dynamic arguments, ]) => @@ -1061,12 +1078,12 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override void setMethodCallHandler( - _i8.Future Function(_i5.MethodCall)? handler) => + _i7.Future Function(_i4.MethodCall)? handler) => super.noSuchMethod( Invocation.method( #setMethodCallHandler, @@ -1080,44 +1097,43 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { /// /// See the documentation for Mockito's code generation for more information. class MockSentryNativeBinding extends _i1.Mock - implements _i13.SentryNativeBinding { + implements _i10.SentryNativeBinding { MockSentryNativeBinding() { _i1.throwOnMissingStub(this); } @override - _i8.Future init(_i14.SentryFlutterOptions? options) => - (super.noSuchMethod( + _i7.Future init(_i2.Hub? hub) => (super.noSuchMethod( Invocation.method( #init, - [options], + [hub], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future close() => (super.noSuchMethod( + _i7.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future<_i15.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( + _i7.Future<_i11.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( Invocation.method( #fetchNativeAppStart, [], ), - returnValue: _i8.Future<_i15.NativeAppStart?>.value(), - ) as _i8.Future<_i15.NativeAppStart?>); + returnValue: _i7.Future<_i11.NativeAppStart?>.value(), + ) as _i7.Future<_i11.NativeAppStart?>); @override - _i8.Future captureEnvelope( - _i16.Uint8List? envelopeData, + _i7.Future captureEnvelope( + _i12.Uint8List? envelopeData, bool? containsUnhandledException, ) => (super.noSuchMethod( @@ -1128,72 +1144,72 @@ class MockSentryNativeBinding extends _i1.Mock containsUnhandledException, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future beginNativeFrames() => (super.noSuchMethod( + _i7.Future beginNativeFrames() => (super.noSuchMethod( Invocation.method( #beginNativeFrames, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future<_i17.NativeFrames?> endNativeFrames(_i3.SentryId? id) => + _i7.Future<_i13.NativeFrames?> endNativeFrames(_i2.SentryId? id) => (super.noSuchMethod( Invocation.method( #endNativeFrames, [id], ), - returnValue: _i8.Future<_i17.NativeFrames?>.value(), - ) as _i8.Future<_i17.NativeFrames?>); + returnValue: _i7.Future<_i13.NativeFrames?>.value(), + ) as _i7.Future<_i13.NativeFrames?>); @override - _i8.Future setUser(_i3.SentryUser? user) => (super.noSuchMethod( + _i7.Future setUser(_i2.SentryUser? user) => (super.noSuchMethod( Invocation.method( #setUser, [user], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future addBreadcrumb(_i3.Breadcrumb? breadcrumb) => + _i7.Future addBreadcrumb(_i2.Breadcrumb? breadcrumb) => (super.noSuchMethod( Invocation.method( #addBreadcrumb, [breadcrumb], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future clearBreadcrumbs() => (super.noSuchMethod( + _i7.Future clearBreadcrumbs() => (super.noSuchMethod( Invocation.method( #clearBreadcrumbs, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future?> loadContexts() => (super.noSuchMethod( + _i7.Future?> loadContexts() => (super.noSuchMethod( Invocation.method( #loadContexts, [], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override - _i8.Future setContexts( + _i7.Future setContexts( String? key, dynamic value, ) => @@ -1205,22 +1221,22 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future removeContexts(String? key) => (super.noSuchMethod( + _i7.Future removeContexts(String? key) => (super.noSuchMethod( Invocation.method( #removeContexts, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future setExtra( + _i7.Future setExtra( String? key, dynamic value, ) => @@ -1232,22 +1248,22 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future removeExtra(String? key) => (super.noSuchMethod( + _i7.Future removeExtra(String? key) => (super.noSuchMethod( Invocation.method( #removeExtra, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future setTag( + _i7.Future setTag( String? key, String? value, ) => @@ -1259,50 +1275,50 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future removeTag(String? key) => (super.noSuchMethod( + _i7.Future removeTag(String? key) => (super.noSuchMethod( Invocation.method( #removeTag, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - int? startProfiler(_i3.SentryId? traceId) => + int? startProfiler(_i2.SentryId? traceId) => (super.noSuchMethod(Invocation.method( #startProfiler, [traceId], )) as int?); @override - _i8.Future discardProfiler(_i3.SentryId? traceId) => + _i7.Future discardProfiler(_i2.SentryId? traceId) => (super.noSuchMethod( Invocation.method( #discardProfiler, [traceId], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future displayRefreshRate() => (super.noSuchMethod( + _i7.Future displayRefreshRate() => (super.noSuchMethod( Invocation.method( #displayRefreshRate, [], ), - returnValue: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future?> collectProfile( - _i3.SentryId? traceId, + _i7.Future?> collectProfile( + _i2.SentryId? traceId, int? startTimeNs, int? endTimeNs, ) => @@ -1315,37 +1331,52 @@ class MockSentryNativeBinding extends _i1.Mock endTimeNs, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override - _i8.Future?> loadDebugImages() => (super.noSuchMethod( + _i7.Future?> loadDebugImages() => (super.noSuchMethod( Invocation.method( #loadDebugImages, [], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override - _i8.Future pauseAppHangTracking() => (super.noSuchMethod( + _i7.Future pauseAppHangTracking() => (super.noSuchMethod( Invocation.method( #pauseAppHangTracking, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future resumeAppHangTracking() => (super.noSuchMethod( + _i7.Future resumeAppHangTracking() => (super.noSuchMethod( Invocation.method( #resumeAppHangTracking, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + + @override + _i7.Future<_i2.SentryId> captureReplay(bool? isCrash) => (super.noSuchMethod( + Invocation.method( + #captureReplay, + [isCrash], + ), + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( + this, + Invocation.method( + #captureReplay, + [isCrash], + ), + )), + ) as _i7.Future<_i2.SentryId>); } /// A class which mocks [Hub]. @@ -1366,13 +1397,13 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.SentryOptions); @override - _i7.MetricsApi get metricsApi => (super.noSuchMethod( + _i5.MetricsApi get metricsApi => (super.noSuchMethod( Invocation.getter(#metricsApi), returnValue: _FakeMetricsApi_11( this, Invocation.getter(#metricsApi), ), - ) as _i7.MetricsApi); + ) as _i5.MetricsApi); @override bool get isEnabled => (super.noSuchMethod( @@ -1381,13 +1412,13 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as bool); @override - _i3.SentryId get lastEventId => (super.noSuchMethod( + _i2.SentryId get lastEventId => (super.noSuchMethod( Invocation.getter(#lastEventId), returnValue: _FakeSentryId_5( this, Invocation.getter(#lastEventId), ), - ) as _i3.SentryId); + ) as _i2.SentryId); @override _i2.Scope get scope => (super.noSuchMethod( @@ -1399,7 +1430,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Scope); @override - set profilerFactory(_i11.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i9.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter( #profilerFactory, value, @@ -1408,8 +1439,8 @@ class MockHub extends _i1.Mock implements _i2.Hub { ); @override - _i8.Future<_i3.SentryId> captureEvent( - _i3.SentryEvent? event, { + _i7.Future<_i2.SentryId> captureEvent( + _i2.SentryEvent? event, { dynamic stackTrace, _i2.Hint? hint, _i2.ScopeCallback? withScope, @@ -1424,7 +1455,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureEvent, @@ -1436,10 +1467,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override - _i8.Future<_i3.SentryId> captureException( + _i7.Future<_i2.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Hint? hint, @@ -1455,7 +1486,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureException, @@ -1467,12 +1498,12 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override - _i8.Future<_i3.SentryId> captureMessage( + _i7.Future<_i2.SentryId> captureMessage( String? message, { - _i3.SentryLevel? level, + _i2.SentryLevel? level, String? template, List? params, _i2.Hint? hint, @@ -1490,7 +1521,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMessage, @@ -1504,22 +1535,22 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override - _i8.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => + _i7.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => (super.noSuchMethod( Invocation.method( #captureUserFeedback, [userFeedback], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.Future addBreadcrumb( - _i3.Breadcrumb? crumb, { + _i7.Future addBreadcrumb( + _i2.Breadcrumb? crumb, { _i2.Hint? hint, }) => (super.noSuchMethod( @@ -1528,9 +1559,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { [crumb], {#hint: hint}, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( @@ -1557,21 +1588,21 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Hub); @override - _i8.Future close() => (super.noSuchMethod( + _i7.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i8.FutureOr configureScope(_i2.ScopeCallback? callback) => + _i7.FutureOr configureScope(_i2.ScopeCallback? callback) => (super.noSuchMethod(Invocation.method( #configureScope, [callback], - )) as _i8.FutureOr); + )) as _i7.FutureOr); @override _i2.ISentrySpan startTransaction( @@ -1604,7 +1635,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i18.startTransactionShim( + returnValue: _i6.startTransactionShim( name, operation, description: description, @@ -1662,8 +1693,8 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.ISentrySpan); @override - _i8.Future<_i3.SentryId> captureTransaction( - _i3.SentryTransaction? transaction, { + _i7.Future<_i2.SentryId> captureTransaction( + _i2.SentryTransaction? transaction, { _i2.SentryTraceContextHeader? traceContext, }) => (super.noSuchMethod( @@ -1672,7 +1703,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [transaction], {#traceContext: traceContext}, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureTransaction, @@ -1680,24 +1711,24 @@ class MockHub extends _i1.Mock implements _i2.Hub { {#traceContext: traceContext}, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override - _i8.Future<_i3.SentryId> captureMetrics( - Map>? metricsBuckets) => + _i7.Future<_i2.SentryId> captureMetrics( + Map>? metricsBuckets) => (super.noSuchMethod( Invocation.method( #captureMetrics, [metricsBuckets], ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i7.Future<_i2.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMetrics, [metricsBuckets], ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i7.Future<_i2.SentryId>); @override void setSpanContext( diff --git a/flutter/test/replay/recorder_config_test.dart b/flutter/test/replay/recorder_config_test.dart new file mode 100644 index 0000000000..d884073e91 --- /dev/null +++ b/flutter/test/replay/recorder_config_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/src/replay/recorder_config.dart'; + +void main() async { + group('$ScreenshotRecorderConfig', () { + test('defaults', () { + var sut = ScreenshotRecorderConfig(); + expect(sut.height, isNull); + expect(sut.width, isNull); + }); + + test('pixel ratio calculation', () { + expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); + expect( + ScreenshotRecorderConfig(width: 5, height: 10) + .getPixelRatio(100, 100), + 0.05); + expect( + ScreenshotRecorderConfig(width: 20, height: 10) + .getPixelRatio(100, 100), + 0.1); + }); + }); +} diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/replay/recorder_test.dart new file mode 100644 index 0000000000..16db1513b5 --- /dev/null +++ b/flutter/test/replay/recorder_test.dart @@ -0,0 +1,48 @@ +// For some reason, this test is not working in the browser but that's OK, we +// don't support video recording anyway. +@TestOn('vm') +library dart_test; + +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/replay/recorder.dart'; +import 'package:sentry_flutter/src/replay/recorder_config.dart'; + +import '../mocks.dart'; +import 'test_widget.dart'; + +void main() async { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('captures images', (tester) async { + final fixture = await _Fixture.create(tester); + expect(fixture.capture(), completion('800x600')); + }); +} + +class _Fixture { + late final ScreenshotRecorder sut; + + _Fixture._() { + sut = ScreenshotRecorder( + ScreenshotRecorderConfig(), + SentryFlutterOptions()..bindingUtils = TestBindingWrapper(), + ); + } + + static Future<_Fixture> create(WidgetTester tester) async { + final fixture = _Fixture._(); + await pumpTestElement(tester); + return fixture; + } + + Future capture() async { + String? captured; + await sut.capture((Image image) async { + captured = "${image.width}x${image.height}"; + }); + return captured; + } +} diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart new file mode 100644 index 0000000000..319bb5f88f --- /dev/null +++ b/flutter/test/replay/replay_native_test.dart @@ -0,0 +1,230 @@ +// ignore_for_file: invalid_use_of_internal_member + +@TestOn('vm') +library flutter_test; + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/event_processor/replay_event_processor.dart'; +import 'package:sentry_flutter/src/native/factory.dart'; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; + +import '../mocks.dart'; +import '../mocks.mocks.dart'; +import 'test_widget.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + for (final mockPlatform in [ + MockPlatform.android(), + MockPlatform.iOs(), + ]) { + group('$SentryNativeBinding ($mockPlatform)', () { + late SentryNativeBinding sut; + late NativeChannelFixture native; + late SentryFlutterOptions options; + late MockHub hub; + late FileSystem fs; + late Directory replayDir; + late final Map replayConfig; + + if (mockPlatform.isIOS) { + replayConfig = { + 'replayId': '123', + 'directory': 'dir', + }; + } else if (mockPlatform.isAndroid) { + replayConfig = { + 'replayId': '123', + 'directory': 'dir', + 'width': 800, + 'height': 600, + 'frameRate': 1000, + }; + } + + setUp(() { + hub = MockHub(); + + fs = MemoryFileSystem.test(); + replayDir = fs.directory(replayConfig['directory']) + ..createSync(recursive: true); + + options = defaultTestOptions() + ..platformChecker = MockPlatformChecker(mockPlatform: mockPlatform) + ..fileSystem = fs; + + native = NativeChannelFixture(); + when(native.handler('initNativeSdk', any)) + .thenAnswer((_) => Future.value()); + when(native.handler('closeNativeSdk', any)) + .thenAnswer((_) => Future.value()); + + sut = createBinding(options, channel: native.channel); + }); + + tearDown(() async { + await sut.close(); + }); + + test('init sets $ReplayEventProcessor when error replay is enabled', + () async { + options.experimental.replay.errorSampleRate = 0.1; + await sut.init(hub); + + expect(options.eventProcessors.map((e) => e.runtimeType.toString()), + contains('$ReplayEventProcessor')); + }); + + test( + 'init does not set $ReplayEventProcessor when error replay is disabled', + () async { + await sut.init(hub); + + expect(options.eventProcessors.map((e) => e.runtimeType.toString()), + isNot(contains('$ReplayEventProcessor'))); + }); + + group('replay recorder', () { + setUp(() async { + options.experimental.replay.sessionSampleRate = 0.1; + options.experimental.replay.errorSampleRate = 0.1; + await sut.init(hub); + }); + + test('sets replay ID to context', () async { + // verify there was no scope configured before + verifyNever(hub.configureScope(any)); + + // emulate the native platform invoking the method + await native.invokeFromNative( + mockPlatform.isAndroid + ? 'ReplayRecorder.start' + : 'captureReplayScreenshot', + replayConfig); + + // verify the replay ID was set + final closure = + verify(hub.configureScope(captureAny)).captured.single; + final scope = Scope(options); + expect(scope.replayId, isNull); + await closure(scope); + expect(scope.replayId.toString(), replayConfig['replayId']); + }); + + test('clears replay ID from context', () async { + // verify there was no scope configured before + verifyNever(hub.configureScope(any)); + + // emulate the native platform invoking the method + await native.invokeFromNative('ReplayRecorder.stop'); + + // verify the replay ID was cleared + final closure = + verify(hub.configureScope(captureAny)).captured.single; + final scope = Scope(options); + scope.replayId = SentryId.newId(); + expect(scope.replayId, isNotNull); + await closure(scope); + expect(scope.replayId, isNull); + }, skip: mockPlatform.isIOS ? 'iOS does not clear replay ID' : false); + + testWidgets('captures images', (tester) async { + await tester.runAsync(() async { + if (mockPlatform.isAndroid) { + var callbackFinished = Completer(); + + nextFrame({bool wait = true}) async { + tester.binding.scheduleFrame(); + await Future.delayed(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await callbackFinished.future.timeout( + Duration(milliseconds: wait ? 1000 : 100), onTimeout: () { + if (wait) { + fail('native callback not called'); + } + }); + callbackFinished = Completer(); + } + + imageInfo(File file) => file.readAsBytesSync().length; + + fileToImageMap(Iterable files) => + {for (var file in files) file.path: imageInfo(file)}; + + final capturedImages = {}; + when(native.handler('addReplayScreenshot', any)) + .thenAnswer((invocation) async { + callbackFinished.complete(); + final path = + invocation.positionalArguments[1]["path"] as String; + capturedImages[path] = imageInfo(fs.file(path)); + return null; + }); + + fsImages() { + final files = replayDir.listSync().map((f) => f as File); + return fileToImageMap(files); + } + + await pumpTestElement(tester); + + await nextFrame(wait: false); + expect(fsImages(), isEmpty); + verifyNever(native.handler('addReplayScreenshot', any)); + + await native.invokeFromNative( + 'ReplayRecorder.start', replayConfig); + + await nextFrame(); + expect(fsImages().values, isNotEmpty); + final size = fsImages().values.first; + expect(size, greaterThan(3000)); + expect(fsImages().values, [size]); + expect(capturedImages, equals(fsImages())); + + await nextFrame(); + expect(fsImages().values, [size, size]); + expect(capturedImages, equals(fsImages())); + + await native.invokeFromNative('ReplayRecorder.stop'); + + await nextFrame(wait: false); + expect(fsImages().values, [size, size]); + expect(capturedImages, equals(fsImages())); + + await nextFrame(wait: false); + expect(fsImages().values, [size, size]); + expect(capturedImages, equals(fsImages())); + } else if (mockPlatform.isIOS) { + // configureScope() is called on iOS + when(hub.configureScope(captureAny)).thenReturn(null); + + nextFrame() async { + tester.binding.scheduleFrame(); + await Future.delayed(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + await pumpTestElement(tester); + await nextFrame(); + + final imagaData = await native.invokeFromNative( + 'captureReplayScreenshot', replayConfig) as ByteData; + expect(imagaData.lengthInBytes, greaterThan(3000)); + } else { + fail('unsupported platform'); + } + }); + }, timeout: Timeout(Duration(seconds: 10))); + }); + }); + } +} diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart new file mode 100644 index 0000000000..f859b27d53 --- /dev/null +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -0,0 +1,63 @@ +// For some reason, this test is not working in the browser but that's OK, we +// don't support video recording anyway. +@TestOn('vm') +library dart_test; + +import 'dart:ui'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/replay/scheduled_recorder.dart'; +import 'package:sentry_flutter/src/replay/recorder_config.dart'; + +import '../mocks.dart'; +import 'test_widget.dart'; + +void main() async { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('captures images', (tester) async { + final fixture = await _Fixture.create(tester); + expect(fixture.capturedImages, isEmpty); + await fixture.nextFrame(); + expect(fixture.capturedImages, ['1000x750']); + await fixture.nextFrame(); + expect(fixture.capturedImages, ['1000x750', '1000x750']); + final stopFuture = fixture.sut.stop(); + await fixture.nextFrame(); + await stopFuture; + expect(fixture.capturedImages, ['1000x750', '1000x750']); + }); +} + +class _Fixture { + final WidgetTester _tester; + late final ScheduledScreenshotRecorder sut; + final capturedImages = []; + + _Fixture._(this._tester) { + sut = ScheduledScreenshotRecorder( + ScheduledScreenshotRecorderConfig( + width: 1000, + height: 1000, + frameRate: 1000, + ), + (Image image) async { + capturedImages.add("${image.width}x${image.height}"); + }, + SentryFlutterOptions()..bindingUtils = TestBindingWrapper(), + ); + } + + static Future<_Fixture> create(WidgetTester tester) async { + final fixture = _Fixture._(tester); + await pumpTestElement(tester); + fixture.sut.start(); + return fixture; + } + + Future nextFrame() async { + _tester.binding.scheduleFrame(); + await _tester.pumpAndSettle(const Duration(seconds: 1)); + } +} diff --git a/flutter/test/replay/scheduler_test.dart b/flutter/test/replay/scheduler_test.dart new file mode 100644 index 0000000000..c41260c854 --- /dev/null +++ b/flutter/test/replay/scheduler_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter/scheduler.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/src/replay/scheduler.dart'; + +void main() { + test('does not trigger callback between frames', () async { + var fixture = _Fixture.started(); + + expect(fixture.calls, 0); + await Future.delayed(const Duration(milliseconds: 100), () {}); + expect(fixture.calls, 0); + }); + + test('triggers callback after a frame', () async { + var fixture = _Fixture(); + fixture.sut.start(); + + expect(fixture.calls, 0); + await fixture.drawFrame(); + expect(fixture.calls, 1); + await fixture.drawFrame(); + await fixture.drawFrame(); + await fixture.drawFrame(); + expect(fixture.calls, 4); + }); + + test('does not trigger when stopped', () async { + var fixture = _Fixture(); + fixture.sut.start(); + + expect(fixture.calls, 0); + await fixture.drawFrame(); + expect(fixture.calls, 1); + await fixture.drawFrame(); + await fixture.sut.stop(); + await fixture.drawFrame(); + expect(fixture.calls, 2); + }); + + test('triggers after a restart', () async { + var fixture = _Fixture(); + fixture.sut.start(); + + expect(fixture.calls, 0); + await fixture.drawFrame(); + expect(fixture.calls, 1); + await fixture.sut.stop(); + await fixture.drawFrame(); + expect(fixture.calls, 1); + fixture.sut.start(); + await fixture.drawFrame(); + expect(fixture.calls, 2); + }); +} + +class _Fixture { + var calls = 0; + late final Scheduler sut; + FrameCallback? registeredCallback; + var _frames = 0; + + _Fixture() { + sut = Scheduler( + const Duration(milliseconds: 1), + (_) async => calls++, + (FrameCallback callback, {String debugLabel = 'callback'}) { + registeredCallback = callback; + }, + ); + } + + factory _Fixture.started() { + return _Fixture()..sut.start(); + } + + Future drawFrame() async { + await Future.delayed(const Duration(milliseconds: 8), () {}); + _frames++; + registeredCallback!(Duration(milliseconds: _frames)); + registeredCallback = null; + } +} diff --git a/flutter/test/replay/test_widget.dart b/flutter/test/replay/test_widget.dart new file mode 100644 index 0000000000..e85dfacaf8 --- /dev/null +++ b/flutter/test/replay/test_widget.dart @@ -0,0 +1,59 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +Future pumpTestElement(WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SentryWidget( + child: SingleChildScrollView( + child: Visibility( + visible: true, + child: Opacity( + opacity: 0.5, + child: Column( + children: [ + newImage(), + const Padding( + padding: EdgeInsets.all(15), + child: Center(child: Text('Centered text')), + ), + ElevatedButton( + onPressed: () {}, + child: Text('Button title'), + ), + newImage(), + // Invisible widgets won't be obscured. + Visibility(visible: false, child: Text('Invisible text')), + Visibility(visible: false, child: newImage()), + Opacity(opacity: 0, child: Text('Invisible text')), + Opacity(opacity: 0, child: newImage()), + Offstage(offstage: true, child: Text('Offstage text')), + Offstage(offstage: true, child: newImage()), + ], + ), + ), + ), + ), + ), + ), + ); + return TestWidgetsFlutterBinding.instance.rootElement!; +} + +Image newImage() => Image.memory( + Uint8List.fromList([ + 66, 77, 142, 0, 0, 0, 0, 0, 0, 0, 138, 0, 0, 0, 124, 0, 0, 0, 1, 0, + 0, 0, 255, 255, 255, 255, 1, 0, 32, 0, 3, 0, 0, 0, 4, 0, 0, 0, 19, + 11, 0, 0, 19, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, + 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 66, 71, 82, 115, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 135, 135, 255, + // This comment prevents dartfmt reformatting this to single-item lines. + ]), + width: 1, + height: 1, + ); diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart new file mode 100644 index 0000000000..3e17f2b5b6 --- /dev/null +++ b/flutter/test/replay/widget_filter_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/src/replay/widget_filter.dart'; + +import 'test_widget.dart'; + +void main() async { + TestWidgetsFlutterBinding.ensureInitialized(); + const defaultBounds = Rect.fromLTRB(0, 0, 1000, 1000); + + final createSut = + ({bool redactImages = false, bool redactText = false}) => WidgetFilter( + logger: (level, message, {exception, logger, stackTrace}) {}, + redactImages: redactImages, + redactText: redactText, + ); + + group('redact text', () { + testWidgets('redacts the correct number of elements', (tester) async { + final sut = createSut(redactText: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 2); + }); + + testWidgets('does not redact text when disabled', (tester) async { + final sut = createSut(redactText: false); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 0); + }); + + testWidgets('does not redact elements that are outside the screen', + (tester) async { + final sut = createSut(redactText: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 100, 100)); + expect(sut.items.length, 1); + }); + }); + + group('redact images', () { + testWidgets('redacts the correct number of elements', (tester) async { + final sut = createSut(redactImages: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 2); + }); + + testWidgets('does not redact text when disabled', (tester) async { + final sut = createSut(redactImages: false); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 0); + }); + + testWidgets('does not redact elements that are outside the screen', + (tester) async { + final sut = createSut(redactImages: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 500, 100)); + expect(sut.items.length, 1); + }); + }); +} diff --git a/flutter/test/screenshot/sentry_screenshot_widget_test.dart b/flutter/test/screenshot/sentry_screenshot_widget_test.dart index 57379387d0..0b6df7ffad 100644 --- a/flutter/test/screenshot/sentry_screenshot_widget_test.dart +++ b/flutter/test/screenshot/sentry_screenshot_widget_test.dart @@ -64,7 +64,6 @@ class Fixture { hub = Hub(_options); return SentryScreenshotWidget( - hub: hub, child: MaterialApp(home: MyApp()), ); } diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index 5ad5eb2b3f..fb339b4682 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -302,6 +302,15 @@ void main() { verify(channel.invokeMethod('resumeAppHangTracking')); }); + + test('nativeCrash', () async { + when(channel.invokeMethod('nativeCrash')) + .thenAnswer((_) => Future.value()); + + await sut.nativeCrash(); + + verify(channel.invokeMethod('nativeCrash')); + }); }); } } diff --git a/flutter/windows/.gitignore b/flutter/windows/.gitignore index 2c36fa939d..808064a0fa 100644 --- a/flutter/windows/.gitignore +++ b/flutter/windows/.gitignore @@ -15,6 +15,3 @@ x86/ *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ - -flutter/example/windows/flutter/generated_plugins.cmake -flutter/example/windows/flutter/generated_plugin_registrant.* \ No newline at end of file diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 1f0a4aff9d..b7d3459e35 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -1,23 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. set(PROJECT_NAME "sentry_flutter") project(${PROJECT_NAME} LANGUAGES CXX) -# This value is used when generating builds using this plugin, so it must -# not be changed -set(PLUGIN_NAME "sentry_flutter_plugin") - -add_library(${PLUGIN_NAME} SHARED - "sentry_flutter_plugin.cpp" -) -apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -target_include_directories(${PLUGIN_NAME} INTERFACE - "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) - -# List of absolute paths to libraries that should be bundled with the plugin +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. set(sentry_flutter_bundled_libraries "" PARENT_SCOPE diff --git a/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h b/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h deleted file mode 100644 index d8482b94cd..0000000000 --- a/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ -#define FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ - -#include - -#ifdef FLUTTER_PLUGIN_IMPL -#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) -#endif - -#if defined(__cplusplus) -extern "C" { -#endif - -FLUTTER_PLUGIN_EXPORT void SentryFlutterPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar); - -#if defined(__cplusplus) -} // extern "C" -#endif - -#endif // FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ diff --git a/flutter/windows/sentry_flutter_plugin.cpp b/flutter/windows/sentry_flutter_plugin.cpp deleted file mode 100644 index 638f4ff16e..0000000000 --- a/flutter/windows/sentry_flutter_plugin.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "include/sentry_flutter/sentry_flutter_plugin.h" - -// This must be included before many other Windows headers. -#include - -// For getPlatformVersion; remove unless needed for your plugin implementation. -#include - -#include -#include -#include - -#include -#include -#include - -namespace { - -class SentryFlutterPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - - SentryFlutterPlugin(); - - virtual ~SentryFlutterPlugin(); - - private: - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result); -}; - -// static -void SentryFlutterPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows *registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "sentry_flutter", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto &call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); -} - -SentryFlutterPlugin::SentryFlutterPlugin() {} - -SentryFlutterPlugin::~SentryFlutterPlugin() {} - -void SentryFlutterPlugin::HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - // Native features will be added in a next release - result->NotImplemented(); -} - -} // namespace - -void SentryFlutterPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - SentryFlutterPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); -} diff --git a/hive/lib/src/version.dart b/hive/lib/src/version.dart index 57e1b4759d..5b2d0edfb4 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.7.0'; +const String sdkVersion = '8.8.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 97a7739681..7abb2c50ab 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 hive: ^2.2.3 meta: ^1.3.0 diff --git a/isar/lib/src/version.dart b/isar/lib/src/version.dart index 675e35663c..e6bbf404ac 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.7.0'; +const String sdkVersion = '8.8.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 65ebc08ba6..55d59747b2 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 meta: ^1.3.0 path: ^1.8.3 diff --git a/logging/lib/src/version.dart b/logging/lib/src/version.dart index 391c1e6341..b6a2fed212 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.7.0'; +const String sdkVersion = '8.8.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 853c311da8..3449fe65ef 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 dev_dependencies: lints: ^4.0.0 diff --git a/metrics/flutter.properties b/metrics/flutter.properties index 23228e31fa..6d6c58697f 100644 --- a/metrics/flutter.properties +++ b/metrics/flutter.properties @@ -1,2 +1,2 @@ -version = 3.24.0 +version = 3.24.1 repo = https://github.com/flutter/flutter diff --git a/sqflite/lib/src/version.dart b/sqflite/lib/src/version.dart index 17a9d7057b..c62c8d1e93 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.7.0'; +const String sdkVersion = '8.8.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 ac73cdd728..43c7b6dcf0 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.7.0 +version: 8.8.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.7.0 + sentry: 8.8.0 sqflite: ^2.2.8 sqflite_common: ^2.0.0 meta: ^1.3.0