diff --git a/dart/lib/src/client_reports/client_report.dart b/dart/lib/src/client_reports/client_report.dart index 38a1a34c67..c00b0b9a5d 100644 --- a/dart/lib/src/client_reports/client_report.dart +++ b/dart/lib/src/client_reports/client_report.dart @@ -1,10 +1,11 @@ import 'package:meta/meta.dart'; +import '../../sentry.dart'; import 'discarded_event.dart'; import '../utils.dart'; @internal -class ClientReport { +class ClientReport implements SentryEnvelopeItemPayload { ClientReport(this.timestamp, this.discardedEvents); final DateTime? timestamp; @@ -27,4 +28,7 @@ class ClientReport { return json; } + + @override + Future getPayload() => Future.value(toJson()); } diff --git a/dart/lib/src/metrics/metric.dart b/dart/lib/src/metrics/metric.dart index fdea81cbf4..ffe6562687 100644 --- a/dart/lib/src/metrics/metric.dart +++ b/dart/lib/src/metrics/metric.dart @@ -216,9 +216,13 @@ class GaugeMetric extends Metric { @visibleForTesting num get last => _last; + num get minimum => _minimum; + num get maximum => _maximum; + num get sum => _sum; + int get count => _count; } @@ -289,3 +293,18 @@ enum MetricType { const MetricType(this.statsdType); } + +@internal +class MetricsData implements SentryEnvelopeItemPayload { + final Map> buckets; + + MetricsData(this.buckets); + + @override + Future getPayload() { + return Future.value(buckets.map((key, value) { + final metrics = value.map((metric) => metric.encodeToStatsd(key)); + return MapEntry(key.toString(), metrics.toList()); + })); + } +} diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 1b2765c426..4fbcbb177c 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; +import '../../sentry.dart'; import '../protocol.dart'; import '../throwable_mechanism.dart'; import '../utils.dart'; @@ -7,7 +8,9 @@ import 'access_aware_map.dart'; /// An event to be reported to Sentry.io. @immutable -class SentryEvent with SentryEventLike { +class SentryEvent + with SentryEventLike + implements SentryEnvelopeItemPayload { /// Creates an event. SentryEvent({ SentryId? eventId, @@ -411,4 +414,9 @@ class SentryEvent with SentryEventLike { if (threadJson?.isNotEmpty ?? false) 'threads': {'values': threadJson}, }; } + + @override + Future getPayload() { + return Future.value(toJson()); + } } diff --git a/dart/lib/src/sentry_attachment/sentry_attachment.dart b/dart/lib/src/sentry_attachment/sentry_attachment.dart index 9998fc895b..12072e2150 100644 --- a/dart/lib/src/sentry_attachment/sentry_attachment.dart +++ b/dart/lib/src/sentry_attachment/sentry_attachment.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import '../../sentry.dart'; import '../protocol/sentry_view_hierarchy.dart'; import '../utils.dart'; @@ -10,7 +11,7 @@ import '../utils.dart'; typedef ContentLoader = FutureOr Function(); /// Arbitrary content which gets attached to an event. -class SentryAttachment { +class SentryAttachment implements SentryEnvelopeItemPayload { /// Standard attachment without special meaning. static const String typeAttachmentDefault = 'event.attachment'; @@ -122,4 +123,9 @@ class SentryAttachment { /// If true, attachment should be added to every transaction. /// Defaults to false. final bool addToTransactions; + + @override + Future getPayload() async { + return await bytes; + } } diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index fb7cd1543a..9eab086521 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -110,7 +110,7 @@ class SentryEnvelope { sdkVersion, dsn: dsn, ), - [SentryEnvelopeItem.fromMetrics(metricsBuckets)], + [SentryEnvelopeItem.fromMetrics(MetricsData(metricsBuckets))], ); } diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index b0f19cfccd..6ff9ff1b58 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -10,11 +10,12 @@ import 'sentry_item_type.dart'; import 'sentry_envelope_item_header.dart'; import 'sentry_user_feedback.dart'; +abstract class SentryEnvelopeItemPayload { + Future getPayload(); +} + /// Item holding header information and JSON encoded data. class SentryEnvelopeItem { - /// The original, non-encoded object, used when direct access to the source data is needed. - Object? originalObject; - SentryEnvelopeItem(this.header, this.dataFactory, {this.originalObject}); /// Creates a [SentryEnvelopeItem] which sends [SentryTransaction]. @@ -91,12 +92,12 @@ class SentryEnvelopeItem { } /// Creates a [SentryEnvelopeItem] which holds several [Metric] data. - factory SentryEnvelopeItem.fromMetrics(Map> buckets) { + factory SentryEnvelopeItem.fromMetrics(MetricsData metricsData) { final cachedItem = _CachedItem(() async { final statsd = StringBuffer(); // Encode all metrics of a bucket in statsd format, using the bucket key, // which is the timestamp of the bucket. - for (final bucket in buckets.entries) { + for (final bucket in metricsData.buckets.entries) { final encodedMetrics = bucket.value.map((metric) => metric.encodeToStatsd(bucket.key)); statsd.write(encodedMetrics.join('\n')); @@ -110,9 +111,12 @@ class SentryEnvelopeItem { contentType: 'application/octet-stream', ); return SentryEnvelopeItem(header, cachedItem.getData, - originalObject: buckets); + originalObject: metricsData); } + /// The original, non-encoded object, used when direct access to the source data is needed. + SentryEnvelopeItemPayload? originalObject; + /// Header with info about type and length of data in bytes. final SentryEnvelopeItemHeader header; diff --git a/dart/lib/src/sentry_user_feedback.dart b/dart/lib/src/sentry_user_feedback.dart index 055199ed61..9b5205c34c 100644 --- a/dart/lib/src/sentry_user_feedback.dart +++ b/dart/lib/src/sentry_user_feedback.dart @@ -1,9 +1,10 @@ import 'package:meta/meta.dart'; +import '../sentry.dart'; import 'protocol.dart'; import 'protocol/access_aware_map.dart'; -class SentryUserFeedback { +class SentryUserFeedback implements SentryEnvelopeItemPayload { SentryUserFeedback({ required this.eventId, this.name, @@ -65,4 +66,9 @@ class SentryUserFeedback { unknown: unknown, ); } + + @override + Future getPayload() { + return Future.value(toJson()); + } } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index f0a6a2c996..1c2ca01621 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -8,3 +8,7 @@ import '../protocol.dart'; abstract class Transport { Future send(SentryEnvelope envelope); } + +abstract class EventTransport { + Future sendEvent(SentryEvent event); +} diff --git a/flutter/example/ios/Runner/AppDelegate.swift b/flutter/example/ios/Runner/AppDelegate.swift index a231cc9c60..c24cacbbb2 100644 --- a/flutter/example/ios/Runner/AppDelegate.swift +++ b/flutter/example/ios/Runner/AppDelegate.swift @@ -2,7 +2,7 @@ import UIKit import Flutter import Sentry -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { private let _channel = "example.flutter.sentry.io" diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 82bcd0b3c8..af2979657c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -20,6 +20,7 @@ import 'package:sentry_isar/sentry_isar.dart'; import 'package:sentry_logging/sentry_logging.dart'; import 'package:sentry_sqflite/sentry_sqflite.dart'; import 'package:sqflite/sqflite.dart'; + // import 'package:sqflite_common_ffi/sqflite_ffi.dart'; // import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; import 'package:universal_platform/universal_platform.dart'; @@ -82,14 +83,18 @@ Future setupSentry( // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; - options.spotlight = Spotlight(enabled: true); - options.enableTimeToFullDisplayTracing = true; + // options.spotlight = Spotlight(enabled: true); + // options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; + options.release = '0.0.2-dart'; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; options.navigatorKey = navigatorKey; + options.experimental.replay.sessionSampleRate = 1; + options.experimental.replay.errorSampleRate = 1; + _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { options.dist = '1'; diff --git a/flutter/lib/src/integrations/web_sdk_integration.dart b/flutter/lib/src/integrations/web_sdk_integration.dart new file mode 100644 index 0000000000..0628337ac7 --- /dev/null +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +/// Initializes the Javascript SDK with the given options. +class WebSdkIntegration implements Integration { + WebSdkIntegration(this._webBinding); + + final SentryWebBinding _webBinding; + SentryFlutterOptions? _options; + + @override + FutureOr call(Hub hub, SentryFlutterOptions options) { + _options = options; + + try { + _webBinding.init(options); + + options.sdk.addIntegration('WebSdkIntegration'); + } catch (exception, stackTrace) { + options.logger( + SentryLevel.fatal, + 'WebSdkIntegration failed to be installed', + exception: exception, + stackTrace: stackTrace, + ); + } + } + + @override + FutureOr close() { + try { + _webBinding.close(); + } catch (exception, stackTrace) { + _options?.logger( + SentryLevel.fatal, + 'WebSdkIntegration failed to be closed', + exception: exception, + stackTrace: stackTrace, + ); + } + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index c688f9862b..7cb028b6d0 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +import 'transport/javascript_transport.dart'; import '../sentry_flutter.dart'; import 'event_processor/android_platform_exception_event_processor.dart'; @@ -10,13 +11,15 @@ import 'event_processor/flutter_enricher_event_processor.dart'; import 'event_processor/flutter_exception_event_processor.dart'; import 'event_processor/platform_exception_event_processor.dart'; import 'event_processor/widget_event_processor.dart'; -import 'file_system_transport.dart'; +import 'transport/file_system_transport.dart'; import 'flutter_exception_type_identifier.dart'; import 'frame_callback_handler.dart'; import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/screenshot_integration.dart'; -import 'native/factory.dart'; +import 'integrations/web_sdk_integration.dart'; +import 'web/factory.dart' as webFactory; +import 'native/factory.dart' as nativeFactory; import 'native/native_scope_observer.dart'; import 'native/sentry_native_binding.dart'; import 'profiling.dart'; @@ -25,6 +28,9 @@ import 'span_frame_metrics_collector.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; +import 'web/sentry_web_binding.dart'; +import 'web_replay_event_processor.dart'; + /// Configuration options callback typedef FlutterOptionsConfiguration = FutureOr Function( SentryFlutterOptions); @@ -66,7 +72,12 @@ mixin SentryFlutter { } if (flutterOptions.platformChecker.hasNativeIntegration) { - _native = createBinding(flutterOptions); + _native = nativeFactory.createBinding(flutterOptions); + } + + // todo: maybe makes sense to combine the bindings into a single interface + if (flutterOptions.platformChecker.isWeb) { + _webBinding = webFactory.createBinding(flutterOptions); } final platformDispatcher = PlatformDispatcher.instance; @@ -129,6 +140,11 @@ mixin SentryFlutter { options.addScopeObserver(NativeScopeObserver(_native!)); } + if (options.platformChecker.isWeb) { + options.transport = JavascriptEnvelopeTransport(_webBinding!, options); + options.addEventProcessor(WebReplayEventProcessor(_webBinding!, options)); + } + options.addEventProcessor(FlutterEnricherEventProcessor(options)); options.addEventProcessor(WidgetEventProcessor()); @@ -190,6 +206,7 @@ mixin SentryFlutter { if (platformChecker.isWeb) { integrations.add(ConnectivityIntegration()); + integrations.add(WebSdkIntegration(_webBinding!)); } // works with Skia, CanvasKit and HTML renderer @@ -280,4 +297,9 @@ mixin SentryFlutter { static set native(SentryNativeBinding? value) => _native = value; static SentryNativeBinding? _native; + + @internal + static SentryWebBinding? get webBinding => _webBinding; + + static SentryWebBinding? _webBinding; } diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 347da9ada3..0e9dd3a470 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -1,8 +1,9 @@ import 'dart:async'; -import 'package:meta/meta.dart'; +import 'package:meta/meta.dart' as meta; import 'package:sentry/sentry.dart'; import 'package:flutter/widgets.dart'; +import 'sentry_replay_options.dart'; import 'binding_wrapper.dart'; import 'renderer/renderer.dart'; @@ -203,14 +204,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. @@ -302,14 +303,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; @@ -319,7 +320,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; @@ -327,6 +328,17 @@ class SentryFlutterOptions extends SentryOptions { /// The [navigatorKey] is used to add information of the currently used locale to the contexts. GlobalKey? navigatorKey; + + /// 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/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart similarity index 88% rename from flutter/lib/src/file_system_transport.dart rename to flutter/lib/src/transport/file_system_transport.dart index 85cc0947a7..f77266923f 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -3,9 +3,10 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import 'package:sentry/sentry.dart'; -import 'native/sentry_native_binding.dart'; +import '../../sentry_flutter.dart'; +// ignore: implementation_imports +import '../native/sentry_native_binding.dart'; class FileSystemTransport implements Transport { FileSystemTransport(this._native, this._options); diff --git a/flutter/lib/src/transport/javascript_transport.dart b/flutter/lib/src/transport/javascript_transport.dart new file mode 100644 index 0000000000..a2718946a8 --- /dev/null +++ b/flutter/lib/src/transport/javascript_transport.dart @@ -0,0 +1,26 @@ +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +class JavascriptEnvelopeTransport implements Transport { + JavascriptEnvelopeTransport(this._binding, this._options); + + final SentryFlutterOptions _options; + final SentryWebBinding _binding; + + @override + Future send(SentryEnvelope envelope) { + try { + _binding.captureEnvelope(envelope); + + return Future.value(SentryId.empty()); + } catch (exception, stackTrace) { + _options.logger( + SentryLevel.error, + 'Failed to send envelope', + exception: exception, + stackTrace: stackTrace, + ); + return Future.value(SentryId.empty()); + } + } +} diff --git a/flutter/lib/src/web/factory.dart b/flutter/lib/src/web/factory.dart new file mode 100644 index 0000000000..dfd2d6c5a8 --- /dev/null +++ b/flutter/lib/src/web/factory.dart @@ -0,0 +1,3 @@ +export 'factory_noop.dart' + if (dart.library.html) 'factory_web.dart' + if (dart.library.js_interop) 'factory_web.dart'; diff --git a/flutter/lib/src/web/factory_noop.dart b/flutter/lib/src/web/factory_noop.dart new file mode 100644 index 0000000000..42c20873c3 --- /dev/null +++ b/flutter/lib/src/web/factory_noop.dart @@ -0,0 +1,30 @@ +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +class NoOpWebInterop implements SentryWebBinding { + @override + Future captureEnvelope(SentryEnvelope envelope) async {} + + @override + Future captureEvent(SentryEvent event) async {} + + @override + Future close() async {} + + @override + Future init(SentryFlutterOptions options) async {} + + @override + Future flushReplay() async {} + + @override + Future getReplayId() { + return Future.value(SentryId.empty()); + } + + @override + Future startReplay() async {} +} + +SentryWebBinding createBinding(SentryFlutterOptions options) => + NoOpWebInterop(); diff --git a/flutter/lib/src/web/factory_web.dart b/flutter/lib/src/web/factory_web.dart new file mode 100644 index 0000000000..dea9cd3e70 --- /dev/null +++ b/flutter/lib/src/web/factory_web.dart @@ -0,0 +1,10 @@ +import 'sentry_js_bridge.dart'; + +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; +import 'sentry_web_interop.dart'; + +SentryWebBinding createBinding(SentryFlutterOptions options, + {SentryJsApi? jsBridge}) { + return SentryWebInterop(jsBridge ?? SentryJsWrapper(), options); +} diff --git a/flutter/lib/src/web/sentry_js_bridge.dart b/flutter/lib/src/web/sentry_js_bridge.dart new file mode 100644 index 0000000000..c6c559c350 --- /dev/null +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -0,0 +1,114 @@ +import 'dart:js_interop'; +import 'package:meta/meta.dart'; + +abstract class SentryJsApi { + void init(JSAny? options); + void close(); + SentryJsClient getClient(); + SentryJsReplay replayIntegration(JSAny? configuration); + JSAny? replayCanvasIntegration(); + JSAny? browserTracingIntegration(); + JSAny? breadcrumbsIntegration(); + SentryJsSession? getSession(); + void captureSession(); +} + +class SentryJsWrapper implements SentryJsApi { + @override + void init(JSAny? options) => _SentryJsBridge.init(options); + + @override + void close() => _SentryJsBridge.close(); + + @override + SentryJsClient getClient() => _SentryJsBridge.getClient(); + + @override + SentryJsReplay replayIntegration(JSAny? configuration) => + _SentryJsBridge.replayIntegration(configuration); + + @override + JSAny? replayCanvasIntegration() => _SentryJsBridge.replayCanvasIntegration(); + + @override + JSAny? browserTracingIntegration() => + _SentryJsBridge.browserTracingIntegration(); + + @override + SentryJsSession? getSession() => _SentryJsBridge.getSession(); + + @override + void captureSession() => _SentryJsBridge.captureSession(); + + @override + JSAny? breadcrumbsIntegration() => _SentryJsBridge.breadcrumbsIntegration(); +} + +@internal +@JS('Sentry') +@staticInterop +class _SentryJsBridge { + external static void init(JSAny? options); + + external static JSAny? replayIntegration(JSAny? configuration); + + external static JSAny? replayCanvasIntegration(); + + external static void close(); + + external static SentryJsClient getClient(); + + external static void captureSession(); + + external static JSAny? browserTracingIntegration(); + + external static JSAny? breadcrumbsIntegration(); + + external static SentryJsScope? getCurrentScope(); + + external static SentryJsScope? getIsolationScope(); + + static SentryJsSession? getSession() { + return getCurrentScope()?.getSession() ?? getIsolationScope()?.getSession(); + } +} + +@JS('Replay') +@staticInterop +class SentryJsReplay {} + +extension SentryReplayExtension on SentryJsReplay { + external void start(); + + external void stop(); + + external JSPromise flush(); + + external JSString? getReplayId(); +} + +@JS('Session') +@staticInterop +class SentryJsSession {} + +extension SentryJsSessionExtension on SentryJsSession { + external JSString status; + + external JSNumber errors; +} + +@JS('Scope') +@staticInterop +class SentryJsScope {} + +extension SentryScopeExtension on SentryJsScope { + external SentryJsSession? getSession(); +} + +@JS('Client') +@staticInterop +class SentryJsClient {} + +extension SentryJsClientExtension on SentryJsClient { + external JSAny? sendEnvelope(JSAny? envelope); +} diff --git a/flutter/lib/src/web/sentry_web_binding.dart b/flutter/lib/src/web/sentry_web_binding.dart new file mode 100644 index 0000000000..e85175e779 --- /dev/null +++ b/flutter/lib/src/web/sentry_web_binding.dart @@ -0,0 +1,17 @@ +import 'package:meta/meta.dart'; + +import '../../sentry_flutter.dart'; + +/// Provide typed methods to access web layer. +@internal +abstract class SentryWebBinding { + Future init(SentryFlutterOptions options); + + Future captureEnvelope(SentryEnvelope envelope); + + Future flushReplay(); + + Future getReplayId(); + + Future close(); +} diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart new file mode 100644 index 0000000000..10143e6d23 --- /dev/null +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -0,0 +1,192 @@ +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:meta/meta.dart'; +import 'sentry_js_bridge.dart'; +import '../../sentry_flutter.dart'; +import '../native/sentry_native_invoker.dart'; +import 'dart:html'; +import 'dart:js_util' as js_util; + +import 'sentry_web_binding.dart'; + +/// Provide typed methods to access native layer via MethodChannel. +@internal +class SentryWebInterop + with SentryNativeSafeInvoker + implements SentryWebBinding { + SentryWebInterop(this._jsBridge, this._options); + + @override + SentryFlutterOptions get options => _options; + final SentryFlutterOptions _options; + final SentryJsApi _jsBridge; + + SentryJsReplay? _replay; + + @override + Future init(SentryFlutterOptions options) async { + return tryCatchAsync('init', () async { + await _loadSentryScripts(options); + + if (!_scriptLoaded) { + options.logger(SentryLevel.warning, + 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); + } + + _replay = _jsBridge.replayIntegration({ + 'maskAllText': options.experimental.replay.redactAllText, + 'blockAllMedia': options.experimental.replay.redactAllImages, + }.jsify()); + + final Map config = { + 'dsn': options.dsn, + 'debug': options.debug, + 'environment': options.environment, + 'release': options.release, + 'dist': options.dist, + 'sampleRate': options.sampleRate, + // 'tracesSampleRate': 1.0, needed if we want to enable some auto performance tracing of JS SDK + 'autoSessionTracking': options.enableAutoSessionTracking, + 'attachStacktrace': options.attachStacktrace, + 'maxBreadcrumbs': options.maxBreadcrumbs, + 'replaysSessionSampleRate': + options.experimental.replay.sessionSampleRate, + 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, + // using defaultIntegrations ensures that we can control which integrations are added + 'defaultIntegrations': [ + _replay, + _jsBridge.replayCanvasIntegration(), + // todo: check which browser integrations make sense + // todo: we have to figure out out how to hook the integration event processing + // from JS to the Flutter layer + // _jsBridge.breadcrumbsIntegration() + // not sure if web vitals are correct, needs more testing + // _jsBridge.browserTracingIntegration() + ], + }; + + // Remove null values to avoid unnecessary properties in the JS object + config.removeWhere((key, value) => value == null); + + _jsBridge.init(config.jsify()); + }); + } + + @override + Future captureEnvelope(SentryEnvelope envelope) async { + return tryCatchAsync('captureEnvelope', () async { + final List envelopeItems = []; + + for (final item in envelope.items) { + final originalObject = item.originalObject; + envelopeItems.add([ + (await item.header.toJson()), + (await originalObject?.getPayload()) + ]); + + // We use `sendEnvelope` where sessions are not managed in the JS SDK + // so we have to do it manually + if (originalObject is SentryEvent && + originalObject.exceptions?.isEmpty == false) { + final session = _jsBridge.getSession(); + if (envelope.containsUnhandledException) { + session?.status = 'crashed'.toJS; + } + session?.errors = originalObject.exceptions?.length.toJS ?? 0.toJS; + _jsBridge.captureSession(); + } + } + + final jsEnvelope = [envelope.header.toJson(), envelopeItems].jsify(); + + _jsBridge.getClient().sendEnvelope(jsEnvelope); + }); + } + + @override + Future close() async { + return tryCatchSync('close', () { + _jsBridge.close(); + }); + } + + @override + Future flushReplay() async { + return tryCatchAsync('flushReplay', () async { + if (_replay == null) { + return; + } + await js_util.promiseToFuture(_replay!.flush()); + }); + } + + @override + Future getReplayId() async { + return tryCatchAsync('getReplayId', () async { + final id = await _replay?.getReplayId()?.toDart; + return id == null ? null : SentryId.fromId(id); + }); + } +} + +bool _scriptLoaded = false; +Future _loadSentryScripts(SentryFlutterOptions options, + {bool useIntegrity = true}) async { + if (_scriptLoaded) return; + + // todo: put this somewhere else so we can auto-update it through CI + List> scripts = [ + { + 'url': + 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js', + 'integrity': + 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' + }, + { + 'url': 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js', + 'integrity': + 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' + }, + ]; + + if (options.debug) { + options.logger(SentryLevel.debug, + 'Option `debug` is enabled, loading non-minified Sentry scripts.'); + scripts = [ + { + 'url': 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.js', + }, + { + 'url': 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.js', + }, + ]; + } + + try { + await Future.wait(scripts.map((script) => _loadScript( + script['url']!, useIntegrity ? script['integrity'] : null))); + _scriptLoaded = true; + options.logger( + SentryLevel.debug, 'All Sentry scripts loaded successfully.'); + } catch (e) { + options.logger(SentryLevel.error, + 'Failed to load Sentry scripts, cannot initialize Sentry JS SDK.'); + } +} + +Future _loadScript(String src, String? integrity) { + final completer = Completer(); + final script = ScriptElement() + ..src = src + ..crossOrigin = 'anonymous' + ..onLoad.listen((_) => completer.complete()) + ..onError.listen((event) => completer.completeError('Failed to load $src')); + + if (integrity != null) { + script.integrity = integrity; + } + + document.head?.append(script); + return completer.future; +} diff --git a/flutter/lib/src/web_replay_event_processor.dart b/flutter/lib/src/web_replay_event_processor.dart new file mode 100644 index 0000000000..3abd108e19 --- /dev/null +++ b/flutter/lib/src/web_replay_event_processor.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import '../sentry_flutter.dart'; +import 'web/sentry_web_binding.dart'; + +class WebReplayEventProcessor implements EventProcessor { + WebReplayEventProcessor(this._binding, this._options); + + final SentryWebBinding _binding; + final SentryFlutterOptions _options; + bool _hasFlushedReplay = false; + + @override + FutureOr apply(SentryEvent event, Hint hint) async { + try { + if (!_options.experimental.replay.isEnabled) { + return event; + } + + // flush the first occurrence of a replay event + // converts buffer to session mode (if the session is set as buffer) + // captures the replay immediately for session mode + if (event.exceptions?.isNotEmpty == true && !_hasFlushedReplay) { + await _binding.flushReplay(); + _hasFlushedReplay = true; + } + + final sentryId = await _binding.getReplayId(); + if (sentryId == null) { + return event; + } + + event = event.copyWith(tags: { + ...?event.tags, + 'replayId': sentryId.toString(), + }); + } catch (exception, stackTrace) { + _options.logger( + SentryLevel.error, + 'Failed to apply $WebReplayEventProcessor', + exception: exception, + stackTrace: stackTrace, + ); + } + return event; + } +} diff --git a/flutter/test/file_system_transport_test.dart b/flutter/test/file_system_transport_test.dart index 5aa0713183..7654c05f32 100644 --- a/flutter/test/file_system_transport_test.dart +++ b/flutter/test/file_system_transport_test.dart @@ -11,7 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; -import 'package:sentry_flutter/src/file_system_transport.dart'; +import 'package:sentry_flutter/src/transport/file_system_transport.dart'; import 'mocks.dart'; import 'mocks.mocks.dart'; diff --git a/flutter/test/integrations/init_web_sdk_test.dart b/flutter/test/integrations/init_web_sdk_test.dart new file mode 100644 index 0000000000..40bb0d45a1 --- /dev/null +++ b/flutter/test/integrations/init_web_sdk_test.dart @@ -0,0 +1,85 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/version.dart'; +import 'package:sentry_flutter/src/web/sentry_js_bridge.dart'; +import 'package:sentry_flutter/src/web/sentry_web_interop.dart'; + +import '../mocks.dart'; + +// todo +void main() { + late Fixture fixture; + setUp(() { + fixture = Fixture(); + }); + + test('test default values', () async { + // String? methodName; + // dynamic arguments; + // + // var sut = fixture.getSut(channel); + // + // await sut.init(fixture.options); + // + // channel.setMethodCallHandler(null); + // + // expect(methodName, 'initNativeSdk'); + // expect(arguments, { + // 'dsn': fakeDsn, + // 'debug': false, + // 'environment': null, + // 'release': null, + // 'enableAutoSessionTracking': true, + // 'enableNativeCrashHandling': true, + // 'attachStacktrace': true, + // 'attachThreads': false, + // 'autoSessionTrackingIntervalMillis': 30000, + // 'dist': null, + // 'integrations': [], + // 'packages': [ + // {'name': 'pub:sentry_flutter', 'version': sdkVersion} + // ], + // 'diagnosticLevel': 'debug', + // 'maxBreadcrumbs': 100, + // 'anrEnabled': false, + // 'anrTimeoutIntervalMillis': 5000, + // 'enableAutoNativeBreadcrumbs': true, + // 'maxCacheItems': 30, + // 'sendDefaultPii': false, + // 'enableWatchdogTerminationTracking': true, + // 'enableNdkScopeSync': true, + // 'enableAutoPerformanceTracing': true, + // 'sendClientReports': true, + // 'proguardUuid': null, + // 'maxAttachmentSize': 20 * 1024 * 1024, + // 'recordHttpBreadcrumbs': true, + // 'captureFailedRequests': true, + // 'enableAppHangTracking': true, + // 'connectionTimeoutMillis': 5000, + // 'readTimeoutMillis': 5000, + // 'appHangTimeoutIntervalMillis': 2000, + // }); + }); +} + +SentryFlutterOptions createOptions() { + final mockPlatformChecker = MockPlatformChecker(isWebValue: true); + final options = SentryFlutterOptions( + dsn: fakeDsn, + checker: mockPlatformChecker, + ); + options.sdk = SdkVersion( + name: sdkName, + version: sdkVersion, + ); + options.sdk.addPackage('pub:sentry_flutter', sdkVersion); + return options; +} + +class Fixture { + late SentryFlutterOptions options; + SentryWebInterop getSut(SentryJsApi jsBridge) { + options = createOptions(); + return SentryWebInterop(jsBridge, options); + } +} diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index f5e5cb65ce..e77aae8100 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -11,6 +11,7 @@ import 'package:meta/meta.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/renderer/renderer.dart'; import 'package:sentry_flutter/src/native/sentry_native_binding.dart'; +import 'package:sentry_flutter/src/web/sentry_js_bridge.dart'; import 'mocks.mocks.dart'; import 'no_such_method_provider.dart'; @@ -47,7 +48,8 @@ ISentrySpan startTransactionShim( SentryTransaction, SentrySpan, MethodChannel, - SentryNativeBinding + SentryNativeBinding, + SentryJsApi, ], customMocks: [ MockSpec(fallbackGenerators: {#startTransaction: startTransactionShim}) ]) diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 01d2127efe..ad9b098eb5 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -3,27 +3,29 @@ // 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 _i9; +import 'dart:js_interop' as _i19; +import 'dart:typed_data' as _i17; 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/src/services/platform_channel.dart' as _i13; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i10; +import 'package:mockito/src/dummies.dart' as _i11; 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/metrics/metric.dart' as _i21; +import 'package:sentry/src/metrics/metrics_api.dart' as _i8; +import 'package:sentry/src/profiling.dart' as _i12; import 'package:sentry/src/protocol.dart' as _i3; -import 'package:sentry/src/sentry_envelope.dart' as _i9; +import 'package:sentry/src/sentry_envelope.dart' as _i10; 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 'package:sentry_flutter/sentry_flutter.dart' as _i15; +import 'package:sentry_flutter/src/native/native_app_start.dart' as _i16; +import 'package:sentry_flutter/src/native/native_frames.dart' as _i18; +import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i14; +import 'package:sentry_flutter/src/web/sentry_js_bridge.dart' as _i7; -import 'mocks.dart' as _i18; +import 'mocks.dart' as _i20; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -142,8 +144,9 @@ class _FakeBinaryMessenger_9 extends _i1.SmartFake ); } -class _FakeSentryOptions_10 extends _i1.SmartFake implements _i2.SentryOptions { - _FakeSentryOptions_10( +class _FakeSentryJsClient_10 extends _i1.SmartFake + implements _i7.SentryJsClient { + _FakeSentryJsClient_10( Object parent, Invocation parentInvocation, ) : super( @@ -152,8 +155,9 @@ class _FakeSentryOptions_10 extends _i1.SmartFake implements _i2.SentryOptions { ); } -class _FakeMetricsApi_11 extends _i1.SmartFake implements _i7.MetricsApi { - _FakeMetricsApi_11( +class _FakeSentryJsReplay_11 extends _i1.SmartFake + implements _i7.SentryJsReplay { + _FakeSentryJsReplay_11( Object parent, Invocation parentInvocation, ) : super( @@ -162,8 +166,8 @@ class _FakeMetricsApi_11 extends _i1.SmartFake implements _i7.MetricsApi { ); } -class _FakeScope_12 extends _i1.SmartFake implements _i2.Scope { - _FakeScope_12( +class _FakeSentryOptions_12 extends _i1.SmartFake implements _i2.SentryOptions { + _FakeSentryOptions_12( Object parent, Invocation parentInvocation, ) : super( @@ -172,8 +176,28 @@ class _FakeScope_12 extends _i1.SmartFake implements _i2.Scope { ); } -class _FakeHub_13 extends _i1.SmartFake implements _i2.Hub { - _FakeHub_13( +class _FakeMetricsApi_13 extends _i1.SmartFake implements _i8.MetricsApi { + _FakeMetricsApi_13( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeScope_14 extends _i1.SmartFake implements _i2.Scope { + _FakeScope_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHub_15 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_15( Object parent, Invocation parentInvocation, ) : super( @@ -191,14 +215,14 @@ class MockTransport extends _i1.Mock implements _i2.Transport { } @override - _i8.Future<_i3.SentryId?> send(_i9.SentryEnvelope? envelope) => + _i9.Future<_i3.SentryId?> send(_i10.SentryEnvelope? envelope) => (super.noSuchMethod( Invocation.method( #send, [envelope], ), - returnValue: _i8.Future<_i3.SentryId?>.value(), - ) as _i8.Future<_i3.SentryId?>); + returnValue: _i9.Future<_i3.SentryId?>.value(), + ) as _i9.Future<_i3.SentryId?>); } /// A class which mocks [SentryTracer]. @@ -212,7 +236,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i10.dummyValue( + returnValue: _i11.dummyValue( this, Invocation.getter(#name), ), @@ -246,7 +270,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profiler(_i11.SentryProfiler? _profiler) => super.noSuchMethod( + set profiler(_i12.SentryProfiler? _profiler) => super.noSuchMethod( Invocation.setter( #profiler, _profiler, @@ -255,7 +279,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ); @override - set profileInfo(_i11.SentryProfileInfo? _profileInfo) => super.noSuchMethod( + set profileInfo(_i12.SentryProfileInfo? _profileInfo) => super.noSuchMethod( Invocation.setter( #profileInfo, _profileInfo, @@ -263,6 +287,12 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { returnValueForMissingStub: null, ); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override _i2.SentrySpanContext get context => (super.noSuchMethod( Invocation.getter(#context), @@ -333,13 +363,7 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { ) as Map); @override - Map get measurements => (super.noSuchMethod( - Invocation.getter(#measurements), - returnValue: {}, - ) as Map); - - @override - _i8.Future finish({ + _i9.Future finish({ _i3.SpanStatus? status, DateTime? endTimestamp, }) => @@ -352,9 +376,9 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { #endTimestamp: endTimestamp, }, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -502,6 +526,24 @@ class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { returnValueForMissingStub: null, ); + @override + void setMeasurementFromChild( + String? name, + num? value, { + _i2.SentryMeasurementUnit? unit, + }) => + super.noSuchMethod( + Invocation.method( + #setMeasurementFromChild, + [ + name, + value, + ], + {#unit: unit}, + ), + returnValueForMissingStub: null, + ); + @override void scheduleFinish() => super.noSuchMethod( Invocation.method( @@ -745,6 +787,15 @@ class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { ), ), ) as _i3.SentryTransaction); + + @override + _i9.Future getPayload() => (super.noSuchMethod( + Invocation.method( + #getPayload, + [], + ), + returnValue: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [SentrySpan]. @@ -834,7 +885,7 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { ) as Map); @override - _i8.Future finish({ + _i9.Future finish({ _i3.SpanStatus? status, DateTime? endTimestamp, }) => @@ -847,9 +898,9 @@ class MockSentrySpan extends _i1.Mock implements _i3.SentrySpan { #endTimestamp: endTimestamp, }, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override void removeData(String? key) => super.noSuchMethod( @@ -984,7 +1035,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 _i13.MethodChannel { MockMethodChannel() { _i1.throwOnMissingStub(this); } @@ -992,7 +1043,7 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { @override String get name => (super.noSuchMethod( Invocation.getter(#name), - returnValue: _i10.dummyValue( + returnValue: _i11.dummyValue( this, Invocation.getter(#name), ), @@ -1017,7 +1068,7 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { ) as _i6.BinaryMessenger); @override - _i8.Future invokeMethod( + _i9.Future invokeMethod( String? method, [ dynamic arguments, ]) => @@ -1029,11 +1080,11 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future?> invokeListMethod( + _i9.Future?> invokeListMethod( String? method, [ dynamic arguments, ]) => @@ -1045,11 +1096,11 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i9.Future?>.value(), + ) as _i9.Future?>); @override - _i8.Future?> invokeMapMethod( + _i9.Future?> invokeMapMethod( String? method, [ dynamic arguments, ]) => @@ -1061,12 +1112,12 @@ class MockMethodChannel extends _i1.Mock implements _i12.MethodChannel { arguments, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i9.Future?>.value(), + ) as _i9.Future?>); @override void setMethodCallHandler( - _i8.Future Function(_i5.MethodCall)? handler) => + _i9.Future Function(_i5.MethodCall)? handler) => super.noSuchMethod( Invocation.method( #setMethodCallHandler, @@ -1080,44 +1131,44 @@ 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 _i14.SentryNativeBinding { MockSentryNativeBinding() { _i1.throwOnMissingStub(this); } @override - _i8.Future init(_i14.SentryFlutterOptions? options) => + _i9.Future init(_i15.SentryFlutterOptions? options) => (super.noSuchMethod( Invocation.method( #init, [options], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future close() => (super.noSuchMethod( + _i9.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future<_i15.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( + _i9.Future<_i16.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( Invocation.method( #fetchNativeAppStart, [], ), - returnValue: _i8.Future<_i15.NativeAppStart?>.value(), - ) as _i8.Future<_i15.NativeAppStart?>); + returnValue: _i9.Future<_i16.NativeAppStart?>.value(), + ) as _i9.Future<_i16.NativeAppStart?>); @override - _i8.Future captureEnvelope( - _i16.Uint8List? envelopeData, + _i9.Future captureEnvelope( + _i17.Uint8List? envelopeData, bool? containsUnhandledException, ) => (super.noSuchMethod( @@ -1128,72 +1179,72 @@ class MockSentryNativeBinding extends _i1.Mock containsUnhandledException, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future beginNativeFrames() => (super.noSuchMethod( + _i9.Future beginNativeFrames() => (super.noSuchMethod( Invocation.method( #beginNativeFrames, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future<_i17.NativeFrames?> endNativeFrames(_i3.SentryId? id) => + _i9.Future<_i18.NativeFrames?> endNativeFrames(_i3.SentryId? id) => (super.noSuchMethod( Invocation.method( #endNativeFrames, [id], ), - returnValue: _i8.Future<_i17.NativeFrames?>.value(), - ) as _i8.Future<_i17.NativeFrames?>); + returnValue: _i9.Future<_i18.NativeFrames?>.value(), + ) as _i9.Future<_i18.NativeFrames?>); @override - _i8.Future setUser(_i3.SentryUser? user) => (super.noSuchMethod( + _i9.Future setUser(_i3.SentryUser? user) => (super.noSuchMethod( Invocation.method( #setUser, [user], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future addBreadcrumb(_i3.Breadcrumb? breadcrumb) => + _i9.Future addBreadcrumb(_i3.Breadcrumb? breadcrumb) => (super.noSuchMethod( Invocation.method( #addBreadcrumb, [breadcrumb], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future clearBreadcrumbs() => (super.noSuchMethod( + _i9.Future clearBreadcrumbs() => (super.noSuchMethod( Invocation.method( #clearBreadcrumbs, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future?> loadContexts() => (super.noSuchMethod( + _i9.Future?> loadContexts() => (super.noSuchMethod( Invocation.method( #loadContexts, [], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i9.Future?>.value(), + ) as _i9.Future?>); @override - _i8.Future setContexts( + _i9.Future setContexts( String? key, dynamic value, ) => @@ -1205,22 +1256,22 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future removeContexts(String? key) => (super.noSuchMethod( + _i9.Future removeContexts(String? key) => (super.noSuchMethod( Invocation.method( #removeContexts, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future setExtra( + _i9.Future setExtra( String? key, dynamic value, ) => @@ -1232,22 +1283,22 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future removeExtra(String? key) => (super.noSuchMethod( + _i9.Future removeExtra(String? key) => (super.noSuchMethod( Invocation.method( #removeExtra, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future setTag( + _i9.Future setTag( String? key, String? value, ) => @@ -1259,19 +1310,19 @@ class MockSentryNativeBinding extends _i1.Mock value, ], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future removeTag(String? key) => (super.noSuchMethod( + _i9.Future removeTag(String? key) => (super.noSuchMethod( Invocation.method( #removeTag, [key], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override int? startProfiler(_i3.SentryId? traceId) => @@ -1281,27 +1332,27 @@ class MockSentryNativeBinding extends _i1.Mock )) as int?); @override - _i8.Future discardProfiler(_i3.SentryId? traceId) => + _i9.Future discardProfiler(_i3.SentryId? traceId) => (super.noSuchMethod( Invocation.method( #discardProfiler, [traceId], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future displayRefreshRate() => (super.noSuchMethod( + _i9.Future displayRefreshRate() => (super.noSuchMethod( Invocation.method( #displayRefreshRate, [], ), - returnValue: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future?> collectProfile( + _i9.Future?> collectProfile( _i3.SentryId? traceId, int? startTimeNs, int? endTimeNs, @@ -1315,37 +1366,104 @@ class MockSentryNativeBinding extends _i1.Mock endTimeNs, ], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i9.Future?>.value(), + ) as _i9.Future?>); @override - _i8.Future?> loadDebugImages() => (super.noSuchMethod( + _i9.Future?> loadDebugImages() => (super.noSuchMethod( Invocation.method( #loadDebugImages, [], ), - returnValue: _i8.Future?>.value(), - ) as _i8.Future?>); + returnValue: _i9.Future?>.value(), + ) as _i9.Future?>); @override - _i8.Future pauseAppHangTracking() => (super.noSuchMethod( + _i9.Future pauseAppHangTracking() => (super.noSuchMethod( Invocation.method( #pauseAppHangTracking, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future resumeAppHangTracking() => (super.noSuchMethod( + _i9.Future resumeAppHangTracking() => (super.noSuchMethod( Invocation.method( #resumeAppHangTracking, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [SentryJsApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSentryJsApi extends _i1.Mock implements _i7.SentryJsApi { + MockSentryJsApi() { + _i1.throwOnMissingStub(this); + } + + @override + void init(_i19.JSAny? options) => super.noSuchMethod( + Invocation.method( + #init, + [options], + ), + returnValueForMissingStub: null, + ); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i7.SentryJsClient getClient() => (super.noSuchMethod( + Invocation.method( + #getClient, + [], + ), + returnValue: _FakeSentryJsClient_10( + this, + Invocation.method( + #getClient, + [], + ), + ), + ) as _i7.SentryJsClient); + + @override + _i7.SentryJsReplay replayIntegration(_i19.JSAny? configuration) => + (super.noSuchMethod( + Invocation.method( + #replayIntegration, + [configuration], + ), + returnValue: _FakeSentryJsReplay_11( + this, + Invocation.method( + #replayIntegration, + [configuration], + ), + ), + ) as _i7.SentryJsReplay); + + @override + void captureSession() => super.noSuchMethod( + Invocation.method( + #captureSession, + [], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [Hub]. @@ -1359,20 +1477,20 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i2.SentryOptions get options => (super.noSuchMethod( Invocation.getter(#options), - returnValue: _FakeSentryOptions_10( + returnValue: _FakeSentryOptions_12( this, Invocation.getter(#options), ), ) as _i2.SentryOptions); @override - _i7.MetricsApi get metricsApi => (super.noSuchMethod( + _i8.MetricsApi get metricsApi => (super.noSuchMethod( Invocation.getter(#metricsApi), - returnValue: _FakeMetricsApi_11( + returnValue: _FakeMetricsApi_13( this, Invocation.getter(#metricsApi), ), - ) as _i7.MetricsApi); + ) as _i8.MetricsApi); @override bool get isEnabled => (super.noSuchMethod( @@ -1392,14 +1510,14 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i2.Scope get scope => (super.noSuchMethod( Invocation.getter(#scope), - returnValue: _FakeScope_12( + returnValue: _FakeScope_14( this, Invocation.getter(#scope), ), ) as _i2.Scope); @override - set profilerFactory(_i11.SentryProfilerFactory? value) => super.noSuchMethod( + set profilerFactory(_i12.SentryProfilerFactory? value) => super.noSuchMethod( Invocation.setter( #profilerFactory, value, @@ -1408,7 +1526,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ); @override - _i8.Future<_i3.SentryId> captureEvent( + _i9.Future<_i3.SentryId> captureEvent( _i3.SentryEvent? event, { dynamic stackTrace, _i2.Hint? hint, @@ -1424,7 +1542,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i9.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureEvent, @@ -1436,10 +1554,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i9.Future<_i3.SentryId>); @override - _i8.Future<_i3.SentryId> captureException( + _i9.Future<_i3.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Hint? hint, @@ -1455,7 +1573,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i9.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureException, @@ -1467,10 +1585,10 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i9.Future<_i3.SentryId>); @override - _i8.Future<_i3.SentryId> captureMessage( + _i9.Future<_i3.SentryId> captureMessage( String? message, { _i3.SentryLevel? level, String? template, @@ -1490,7 +1608,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i9.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMessage, @@ -1504,21 +1622,21 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i9.Future<_i3.SentryId>); @override - _i8.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => + _i9.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => (super.noSuchMethod( Invocation.method( #captureUserFeedback, [userFeedback], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future addBreadcrumb( + _i9.Future addBreadcrumb( _i3.Breadcrumb? crumb, { _i2.Hint? hint, }) => @@ -1528,9 +1646,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { [crumb], {#hint: hint}, ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( @@ -1547,7 +1665,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #clone, [], ), - returnValue: _FakeHub_13( + returnValue: _FakeHub_15( this, Invocation.method( #clone, @@ -1557,21 +1675,21 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.Hub); @override - _i8.Future close() => (super.noSuchMethod( + _i9.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.FutureOr configureScope(_i2.ScopeCallback? callback) => + _i9.FutureOr configureScope(_i2.ScopeCallback? callback) => (super.noSuchMethod(Invocation.method( #configureScope, [callback], - )) as _i8.FutureOr); + )) as _i9.FutureOr); @override _i2.ISentrySpan startTransaction( @@ -1604,7 +1722,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #customSamplingContext: customSamplingContext, }, ), - returnValue: _i18.startTransactionShim( + returnValue: _i20.startTransactionShim( name, operation, description: description, @@ -1662,7 +1780,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ) as _i2.ISentrySpan); @override - _i8.Future<_i3.SentryId> captureTransaction( + _i9.Future<_i3.SentryId> captureTransaction( _i3.SentryTransaction? transaction, { _i2.SentryTraceContextHeader? traceContext, }) => @@ -1672,7 +1790,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [transaction], {#traceContext: traceContext}, ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i9.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureTransaction, @@ -1680,24 +1798,24 @@ class MockHub extends _i1.Mock implements _i2.Hub { {#traceContext: traceContext}, ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i9.Future<_i3.SentryId>); @override - _i8.Future<_i3.SentryId> captureMetrics( - Map>? metricsBuckets) => + _i9.Future<_i3.SentryId> captureMetrics( + Map>? metricsBuckets) => (super.noSuchMethod( Invocation.method( #captureMetrics, [metricsBuckets], ), - returnValue: _i8.Future<_i3.SentryId>.value(_FakeSentryId_5( + returnValue: _i9.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMetrics, [metricsBuckets], ), )), - ) as _i8.Future<_i3.SentryId>); + ) as _i9.Future<_i3.SentryId>); @override void setSpanContext( diff --git a/flutter/test/sentry_flutter_util.dart b/flutter/test/sentry_flutter_util.dart index 7397c4a6a5..08f61c6e7d 100644 --- a/flutter/test/sentry_flutter_util.dart +++ b/flutter/test/sentry_flutter_util.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/file_system_transport.dart'; +import 'package:sentry_flutter/src/transport/file_system_transport.dart'; import 'package:sentry_flutter/src/native/native_scope_observer.dart'; void testTransport({