From d7eac95f16c30944b170551a84a33959ab632aeb Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 6 Aug 2024 22:58:18 +0200 Subject: [PATCH 01/15] add js sdk --- flutter/example/lib/main.dart | 16 ++++++-- flutter/lib/sentry_flutter.dart | 1 + flutter/lib/src/test_web.dart | 69 +++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 flutter/lib/src/test_web.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 784a6b30df..52008544aa 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'; @@ -54,6 +55,14 @@ Future main() async { ), exampleDsn, ); + + loadSentryJS(); + + Future.delayed(const Duration(seconds: 2), () async { + await initSentryJS((options) { + options.dsn = exampleDsn; + }); + }); } Future setupSentry( @@ -774,9 +783,10 @@ void navigateToAutoCloseScreen(BuildContext context) { Future tryCatch() async { try { - throw StateError('try catch'); - } catch (error, stackTrace) { - await Sentry.captureException(error, stackTrace: stackTrace); + // Some code that might throw + throw Exception('Test exception'); + } catch (e, stackTrace) { + SentryJS.captureException(e); } } diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index d15c8b7a70..4c3f813bd5 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -17,3 +17,4 @@ export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; export 'src/navigation/sentry_display_widget.dart'; +export 'src/test_web.dart'; diff --git a/flutter/lib/src/test_web.dart b/flutter/lib/src/test_web.dart new file mode 100644 index 0000000000..71eb332a24 --- /dev/null +++ b/flutter/lib/src/test_web.dart @@ -0,0 +1,69 @@ +@JS() +library web_sentry_loader; + +import 'dart:async'; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' as js_util; +import 'dart:html'; + +import '../sentry_flutter.dart'; + +@JS('Sentry') +class SentryJS { + external static void init(dynamic options); + + external static dynamic captureException(dynamic exception); + + external static dynamic captureMessage(String message); + + external static dynamic replayIntegration(dynamic configuration); + + external static dynamic replayCanvasIntegration(); +} + +void loadSentryJS() { + // if (_sentry != null) return; // Already loaded + + final script = ScriptElement() + ..src = 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js' + ..integrity = + 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' + ..crossOrigin = 'anonymous'; + + document.head!.append(script); + + // src="https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js" + // integrity="sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE" + // crossorigin="anonymous" + + final script2 = ScriptElement() + ..src = 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js' + ..integrity = + 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' + ..crossOrigin = 'anonymous'; + + document.head!.append(script2); +} + +Future initSentryJS( + FutureOr Function(SentryOptions) configuration) async { + final options = SentryOptions(); + await configuration(options); + + final jsOptions = js_util.jsify({ + 'dsn': options.dsn, + 'debug': true, + 'replaysSessionSampleRate': 1.0, + 'replaysOnErrorSampleRate': 1.0, + 'integrations': [ + SentryJS.replayIntegration(js_util.jsify({ + 'maskAllText': false, + 'blockAllMedia': false, + })), + SentryJS.replayCanvasIntegration(), + ], + }); + + SentryJS.init(jsOptions); +} From 66d643c0a3e0b636c2f61130438cdb7e8929444c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 16:41:35 +0200 Subject: [PATCH 02/15] web sdk --- flutter/example/lib/main.dart | 16 +-- flutter/lib/sentry_flutter.dart | 1 - flutter/lib/src/file_system_transport.dart | 53 ++++++- .../src/integrations/web_sdk_integration.dart | 42 ++++++ flutter/lib/src/sentry_flutter.dart | 22 +++ flutter/lib/src/sentry_flutter_options.dart | 24 +++- flutter/lib/src/sentry_replay_options.dart | 40 ++++++ flutter/lib/src/test_web.dart | 69 --------- flutter/lib/src/web/sentry_js_bridge.dart | 30 ++++ .../lib/src/web/sentry_safe_web_interop.dart | 13 ++ flutter/lib/src/web/sentry_web_binding.dart | 15 ++ flutter/lib/src/web/sentry_web_interop.dart | 135 ++++++++++++++++++ 12 files changed, 373 insertions(+), 87 deletions(-) create mode 100644 flutter/lib/src/integrations/web_sdk_integration.dart create mode 100644 flutter/lib/src/sentry_replay_options.dart delete mode 100644 flutter/lib/src/test_web.dart create mode 100644 flutter/lib/src/web/sentry_js_bridge.dart create mode 100644 flutter/lib/src/web/sentry_safe_web_interop.dart create mode 100644 flutter/lib/src/web/sentry_web_binding.dart create mode 100644 flutter/lib/src/web/sentry_web_interop.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 39335f90c3..257921740e 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -55,14 +55,6 @@ Future main() async { ), exampleDsn, ); - - loadSentryJS(); - - Future.delayed(const Duration(seconds: 2), () async { - await initSentryJS((options) { - options.dsn = exampleDsn; - }); - }); } Future setupSentry( @@ -92,13 +84,16 @@ Future setupSentry( // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; options.spotlight = Spotlight(enabled: true); - options.enableTimeToFullDisplayTracing = true; + // options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; options.maxRequestBodySize = MaxRequestBodySize.always; 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'; @@ -786,7 +781,8 @@ Future tryCatch() async { // Some code that might throw throw Exception('Test exception'); } catch (e, stackTrace) { - SentryJS.captureException(e); + Sentry.captureException(e, stackTrace: stackTrace); + // SentryJS.captureException(e); } } diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 4c3f813bd5..d15c8b7a70 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -17,4 +17,3 @@ export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; export 'src/sentry_widget.dart'; export 'src/navigation/sentry_display_widget.dart'; -export 'src/test_web.dart'; diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 85cc0947a7..b08a5eb7ad 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -3,9 +3,15 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import 'package:sentry/sentry.dart'; +import '../sentry_flutter.dart'; +// ignore: implementation_imports +import 'package:sentry/src/transport/http_transport.dart'; import 'native/sentry_native_binding.dart'; +import 'package:js/js_util.dart' as js_util; + +import 'web/sentry_js_bridge.dart'; +import 'web/sentry_web_binding.dart'; class FileSystemTransport implements Transport { FileSystemTransport(this._native, this._options); @@ -34,3 +40,48 @@ class FileSystemTransport implements Transport { return envelope.header.eventId; } } + +class EventTransportAdapter implements Transport { + final EventTransport _eventTransport; + final Transport _envelopeTransport; + + EventTransportAdapter(this._eventTransport, this._envelopeTransport); + + @override + Future send(SentryEnvelope envelope) async { + for (final item in envelope.items) { + final object = item.originalObject; + if (item.header.type == 'event' && object is SentryEvent) { + return _eventTransport.sendEvent(object); + } + } + // If no event is found in the envelope, return an empty ID + return SentryId.empty(); + } +} + +class JavascriptEventTransport implements EventTransport { + final SentryWebBinding _binding; + + JavascriptEventTransport(this._binding); + + @override + Future sendEvent(SentryEvent event) { + _binding.captureEvent(event); + + return Future.value(event.eventId); + } +} + +class JavascriptEnvelopeTransport implements Transport { + final SentryWebBinding _binding; + + JavascriptEnvelopeTransport(this._binding); + + @override + Future send(SentryEnvelope envelope) { + _binding.captureEnvelope(envelope); + + return Future.value(SentryId.empty()); + } +} 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..39e2acd9a0 --- /dev/null +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +class WebSdkIntegration implements Integration { + final SentryWebBinding _binding; + SentryFlutterOptions? _options; + + WebSdkIntegration(this._binding); + + @override + FutureOr call(Hub hub, SentryFlutterOptions options) { + _options = options; + + try { + _binding.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 { + _binding.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..1f690db05e 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -16,6 +16,7 @@ import 'frame_callback_handler.dart'; import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/screenshot_integration.dart'; +import 'integrations/web_sdk_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; import 'native/sentry_native_binding.dart'; @@ -24,6 +25,13 @@ import 'renderer/renderer.dart'; import 'span_frame_metrics_collector.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; +// ignore: implementation_imports +import 'package:sentry/src/transport/http_transport.dart'; +// ignore: implementation_imports +import 'package:sentry/src/transport/rate_limiter.dart'; + +import 'web/sentry_web_binding.dart'; +import 'web/sentry_web_interop.dart'; /// Configuration options callback typedef FlutterOptionsConfiguration = FutureOr Function( @@ -69,6 +77,10 @@ mixin SentryFlutter { _native = createBinding(flutterOptions); } + if (flutterOptions.platformChecker.isWeb) { + _webBinding = SentryWebInterop(flutterOptions); + } + final platformDispatcher = PlatformDispatcher.instance; final wrapper = PlatformDispatcherWrapper(platformDispatcher); @@ -129,6 +141,13 @@ mixin SentryFlutter { options.addScopeObserver(NativeScopeObserver(_native!)); } + if (options.platformChecker.isWeb) { + final eventTransport = JavascriptEventTransport(_webBinding!); + final envelopeTransport = JavascriptEnvelopeTransport(_webBinding!); + options.transport = + EventTransportAdapter(eventTransport, envelopeTransport); + } + options.addEventProcessor(FlutterEnricherEventProcessor(options)); options.addEventProcessor(WidgetEventProcessor()); @@ -190,6 +209,7 @@ mixin SentryFlutter { if (platformChecker.isWeb) { integrations.add(ConnectivityIntegration()); + integrations.add(WebSdkIntegration(_webBinding!)); } // works with Skia, CanvasKit and HTML renderer @@ -280,4 +300,6 @@ mixin SentryFlutter { static set native(SentryNativeBinding? value) => _native = value; static SentryNativeBinding? _native; + + 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/test_web.dart b/flutter/lib/src/test_web.dart deleted file mode 100644 index 71eb332a24..0000000000 --- a/flutter/lib/src/test_web.dart +++ /dev/null @@ -1,69 +0,0 @@ -@JS() -library web_sentry_loader; - -import 'dart:async'; - -import 'package:js/js.dart'; -import 'package:js/js_util.dart' as js_util; -import 'dart:html'; - -import '../sentry_flutter.dart'; - -@JS('Sentry') -class SentryJS { - external static void init(dynamic options); - - external static dynamic captureException(dynamic exception); - - external static dynamic captureMessage(String message); - - external static dynamic replayIntegration(dynamic configuration); - - external static dynamic replayCanvasIntegration(); -} - -void loadSentryJS() { - // if (_sentry != null) return; // Already loaded - - final script = ScriptElement() - ..src = 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js' - ..integrity = - 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' - ..crossOrigin = 'anonymous'; - - document.head!.append(script); - - // src="https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js" - // integrity="sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE" - // crossorigin="anonymous" - - final script2 = ScriptElement() - ..src = 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js' - ..integrity = - 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' - ..crossOrigin = 'anonymous'; - - document.head!.append(script2); -} - -Future initSentryJS( - FutureOr Function(SentryOptions) configuration) async { - final options = SentryOptions(); - await configuration(options); - - final jsOptions = js_util.jsify({ - 'dsn': options.dsn, - 'debug': true, - 'replaysSessionSampleRate': 1.0, - 'replaysOnErrorSampleRate': 1.0, - 'integrations': [ - SentryJS.replayIntegration(js_util.jsify({ - 'maskAllText': false, - 'blockAllMedia': false, - })), - SentryJS.replayCanvasIntegration(), - ], - }); - - SentryJS.init(jsOptions); -} 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..b1460fa600 --- /dev/null +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -0,0 +1,30 @@ +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +@internal +@JS('Sentry') +class SentryJsBridge { + external static void init(dynamic options); + + external static void close(); + + external static dynamic captureException(dynamic exception); + + external static dynamic captureMessage(String message); + + external static dynamic captureEvent(dynamic event); + + external static dynamic replayIntegration(dynamic configuration); + + external static dynamic replayCanvasIntegration(); + + external static SentryJsClient getClient(); +} + +@JS('Client') +@staticInterop +class SentryJsClient {} + +extension SentryJsClientExtension on SentryJsClient { + external dynamic sendEnvelope(dynamic envelope); +} diff --git a/flutter/lib/src/web/sentry_safe_web_interop.dart b/flutter/lib/src/web/sentry_safe_web_interop.dart new file mode 100644 index 0000000000..a5470dcec1 --- /dev/null +++ b/flutter/lib/src/web/sentry_safe_web_interop.dart @@ -0,0 +1,13 @@ +import 'sentry_js_bridge.dart'; + +import '../../sentry_flutter.dart'; +import '../native/sentry_native_invoker.dart'; + +class SentrySafeMethodChannel with SentryNativeSafeInvoker { + @override + final SentryFlutterOptions options; + + final SentryJsBridge _bridge; + + SentrySafeMethodChannel(this._bridge, this.options); +} 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..349a91e084 --- /dev/null +++ b/flutter/lib/src/web/sentry_web_binding.dart @@ -0,0 +1,15 @@ +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 captureEvent(SentryEvent event); + + 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..afd81ac705 --- /dev/null +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:sentry/src/protocol/sentry_event.dart'; +import 'package:sentry/src/sentry_envelope.dart'; +import '../../sentry_flutter.dart'; +import 'sentry_js_bridge.dart'; +import 'dart:html'; + +import '../sentry_flutter_options.dart'; +import 'sentry_web_binding.dart'; +import 'package:js/js_util.dart' as js_util; + +/// Provide typed methods to access native layer via MethodChannel. +@internal +class SentryWebInterop implements SentryWebBinding { + final SentryFlutterOptions _options; + + SentryWebInterop(this._options); + + @override + Future init(SentryFlutterOptions options) async { + await _loadSentryScripts(options); + + if (!_scriptLoaded) { + options.logger(SentryLevel.warning, + 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); + } + + final Map config = { + 'dsn': options.dsn, + 'debug': options.debug, + 'environment': options.environment, + 'release': options.release, + 'dist': options.dist, + 'autoSessionTracking': options.enableAutoSessionTracking, + 'attachStacktrace': options.attachStacktrace, + 'maxBreadcrumbs': options.maxBreadcrumbs, + 'replaysSessionSampleRate': options.experimental.replay.sessionSampleRate, + 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, + 'defaultIntegrations': [ + SentryJsBridge.replayIntegration(js_util.jsify({ + 'maskAllText': options.experimental.replay.redactAllText, + // todo: is redactAllImages the same? + 'blockAllMedia': options.experimental.replay.redactAllImages, + })), + SentryJsBridge.replayCanvasIntegration(), + ], + }; + + // Remove null values to avoid unnecessary properties in the JS object + config.removeWhere((key, value) => value == null); + + SentryJsBridge.init(js_util.jsify(config)); + + return Future.value(); + } + + @override + Future captureEvent(SentryEvent event) { + SentryJsBridge.captureEvent(js_util.jsify(event.toJson())); + + return Future.value(); + } + + @override + Future captureEnvelope(SentryEnvelope envelope) async { + SentryJsBridge.getClient().sendEnvelope(js_util.jsify([ + envelope.header.toJson(), + [ + envelope.items.map((item) { + return [ + item.header.toJson(), + item.originalObject, + ]; + }).toList(), + ] + ])); + + return Future.value(); + } + + @override + Future close() { + SentryJsBridge.close(); + + return Future.value(); + } +} + +bool _scriptLoaded = false; + +Future _loadSentryScripts(SentryFlutterOptions options) async { + if (_scriptLoaded) return; + + List scriptUrls = [ + 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js', + 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js' + ]; + + Map integrityHashes = { + 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js': + 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL', + 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js': + 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' + }; + + List> loadFutures = []; + + for (String url in scriptUrls) { + loadFutures.add(_loadScript(url, integrityHashes[url]!)); + } + + try { + await Future.wait(loadFutures); + _scriptLoaded = true; + print('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) { + Completer completer = Completer(); + ScriptElement script = ScriptElement() + ..src = src + ..integrity = integrity + ..crossOrigin = 'anonymous' + ..onLoad.listen((_) => completer.complete()) + ..onError.listen((event) => completer.completeError('Failed to load $src')); + + document.head?.append(script); + return completer.future; +} From 2870f34174b03c1fab10c4efc8b188451557d228 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 18:24:19 +0200 Subject: [PATCH 03/15] update --- dart/lib/src/sentry_client.dart | 2 +- dart/lib/src/transport/transport.dart | 4 + .../src/integrations/web_sdk_integration.dart | 9 +- flutter/lib/src/sentry_flutter.dart | 7 +- .../file_system_transport.dart | 37 +--- .../src/transport/javascript_transport.dart | 28 +++ flutter/lib/src/web/sentry_js_bridge.dart | 30 +-- .../lib/src/web/sentry_safe_web_interop.dart | 13 -- flutter/lib/src/web/sentry_web_interop.dart | 179 ++++++++++-------- flutter/test/file_system_transport_test.dart | 2 +- flutter/test/sentry_flutter_util.dart | 2 +- 11 files changed, 160 insertions(+), 153 deletions(-) rename flutter/lib/src/{ => transport}/file_system_transport.dart (64%) create mode 100644 flutter/lib/src/transport/javascript_transport.dart delete mode 100644 flutter/lib/src/web/sentry_safe_web_interop.dart diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index c4ebac3db5..bcdc9b68a8 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -187,7 +187,7 @@ class SentryClient { ); final id = await captureEnvelope(envelope); - return id ?? SentryId.empty(); + return SentryId.empty(); } bool _isIgnoredError(SentryEvent event) { 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/lib/src/integrations/web_sdk_integration.dart b/flutter/lib/src/integrations/web_sdk_integration.dart index 39e2acd9a0..2b16a46ae2 100644 --- a/flutter/lib/src/integrations/web_sdk_integration.dart +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -3,18 +3,19 @@ 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 { - final SentryWebBinding _binding; + final SentryWebBinding _webBinding; SentryFlutterOptions? _options; - WebSdkIntegration(this._binding); + WebSdkIntegration(this._webBinding); @override FutureOr call(Hub hub, SentryFlutterOptions options) { _options = options; try { - _binding.init(options); + _webBinding.init(options); options.sdk.addIntegration('WebSdkIntegration'); } catch (exception, stackTrace) { options.logger( @@ -29,7 +30,7 @@ class WebSdkIntegration implements Integration { @override FutureOr close() { try { - _binding.close(); + _webBinding.close(); } catch (exception, stackTrace) { _options?.logger( SentryLevel.fatal, diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 1f690db05e..cb16abed5e 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,7 +11,7 @@ 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'; @@ -25,10 +26,6 @@ import 'renderer/renderer.dart'; import 'span_frame_metrics_collector.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; -// ignore: implementation_imports -import 'package:sentry/src/transport/http_transport.dart'; -// ignore: implementation_imports -import 'package:sentry/src/transport/rate_limiter.dart'; import 'web/sentry_web_binding.dart'; import 'web/sentry_web_interop.dart'; diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart similarity index 64% rename from flutter/lib/src/file_system_transport.dart rename to flutter/lib/src/transport/file_system_transport.dart index b08a5eb7ad..8faad0822d 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -4,14 +4,9 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; -import '../sentry_flutter.dart'; +import '../../sentry_flutter.dart'; // ignore: implementation_imports -import 'package:sentry/src/transport/http_transport.dart'; -import 'native/sentry_native_binding.dart'; -import 'package:js/js_util.dart' as js_util; - -import 'web/sentry_js_bridge.dart'; -import 'web/sentry_web_binding.dart'; +import '../native/sentry_native_binding.dart'; class FileSystemTransport implements Transport { FileSystemTransport(this._native, this._options); @@ -53,35 +48,11 @@ class EventTransportAdapter implements Transport { final object = item.originalObject; if (item.header.type == 'event' && object is SentryEvent) { return _eventTransport.sendEvent(object); + } else { + return _envelopeTransport.send(envelope); } } // If no event is found in the envelope, return an empty ID return SentryId.empty(); } } - -class JavascriptEventTransport implements EventTransport { - final SentryWebBinding _binding; - - JavascriptEventTransport(this._binding); - - @override - Future sendEvent(SentryEvent event) { - _binding.captureEvent(event); - - return Future.value(event.eventId); - } -} - -class JavascriptEnvelopeTransport implements Transport { - final SentryWebBinding _binding; - - JavascriptEnvelopeTransport(this._binding); - - @override - Future send(SentryEnvelope envelope) { - _binding.captureEnvelope(envelope); - - return Future.value(SentryId.empty()); - } -} diff --git a/flutter/lib/src/transport/javascript_transport.dart b/flutter/lib/src/transport/javascript_transport.dart new file mode 100644 index 0000000000..107eec1b65 --- /dev/null +++ b/flutter/lib/src/transport/javascript_transport.dart @@ -0,0 +1,28 @@ +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +class JavascriptEventTransport implements EventTransport { + final SentryWebBinding _binding; + + JavascriptEventTransport(this._binding); + + @override + Future sendEvent(SentryEvent event) { + _binding.captureEvent(event); + + return Future.value(event.eventId); + } +} + +class JavascriptEnvelopeTransport implements Transport { + final SentryWebBinding _binding; + + JavascriptEnvelopeTransport(this._binding); + + @override + Future send(SentryEnvelope envelope) { + _binding.captureEnvelope(envelope); + + return Future.value(SentryId.empty()); + } +} diff --git a/flutter/lib/src/web/sentry_js_bridge.dart b/flutter/lib/src/web/sentry_js_bridge.dart index b1460fa600..77ed9710d0 100644 --- a/flutter/lib/src/web/sentry_js_bridge.dart +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -1,30 +1,36 @@ -import 'package:js/js.dart'; +import 'dart:js_interop'; import 'package:meta/meta.dart'; +@internal +@JS('Spotlight') +@staticInterop +class SpotlightBridge { + external static void init(); +} + @internal @JS('Sentry') +@staticInterop class SentryJsBridge { - external static void init(dynamic options); + external static void init(JSAny? options); external static void close(); - external static dynamic captureException(dynamic exception); - - external static dynamic captureMessage(String message); + external static JSAny? captureMessage(JSString message); - external static dynamic captureEvent(dynamic event); + external static JSString captureEvent(JSAny? event); - external static dynamic replayIntegration(dynamic configuration); + external static JSAny? replayIntegration(JSAny? configuration); - external static dynamic replayCanvasIntegration(); + external static JSAny? replayCanvasIntegration(); - external static SentryJsClient getClient(); + external static _SentryJsClient getClient(); } @JS('Client') @staticInterop -class SentryJsClient {} +class _SentryJsClient {} -extension SentryJsClientExtension on SentryJsClient { - external dynamic sendEnvelope(dynamic envelope); +extension SentryJsClientExtension on _SentryJsClient { + external JSAny? sendEnvelope(JSAny? envelope); } diff --git a/flutter/lib/src/web/sentry_safe_web_interop.dart b/flutter/lib/src/web/sentry_safe_web_interop.dart deleted file mode 100644 index a5470dcec1..0000000000 --- a/flutter/lib/src/web/sentry_safe_web_interop.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'sentry_js_bridge.dart'; - -import '../../sentry_flutter.dart'; -import '../native/sentry_native_invoker.dart'; - -class SentrySafeMethodChannel with SentryNativeSafeInvoker { - @override - final SentryFlutterOptions options; - - final SentryJsBridge _bridge; - - SentrySafeMethodChannel(this._bridge, this.options); -} diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index afd81ac705..4dbfe00322 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -1,135 +1,148 @@ import 'dart:async'; +import 'dart:js_interop'; import 'package:meta/meta.dart'; -import 'package:sentry/src/protocol/sentry_event.dart'; -import 'package:sentry/src/sentry_envelope.dart'; import '../../sentry_flutter.dart'; +import '../native/sentry_native_invoker.dart'; import 'sentry_js_bridge.dart'; import 'dart:html'; -import '../sentry_flutter_options.dart'; import 'sentry_web_binding.dart'; -import 'package:js/js_util.dart' as js_util; /// Provide typed methods to access native layer via MethodChannel. @internal -class SentryWebInterop implements SentryWebBinding { +class SentryWebInterop + with SentryNativeSafeInvoker + implements SentryWebBinding { + @override + SentryFlutterOptions get options => _options; final SentryFlutterOptions _options; SentryWebInterop(this._options); @override Future init(SentryFlutterOptions options) async { - await _loadSentryScripts(options); - - if (!_scriptLoaded) { - options.logger(SentryLevel.warning, - 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); - } - - final Map config = { - 'dsn': options.dsn, - 'debug': options.debug, - 'environment': options.environment, - 'release': options.release, - 'dist': options.dist, - 'autoSessionTracking': options.enableAutoSessionTracking, - 'attachStacktrace': options.attachStacktrace, - 'maxBreadcrumbs': options.maxBreadcrumbs, - 'replaysSessionSampleRate': options.experimental.replay.sessionSampleRate, - 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, - 'defaultIntegrations': [ - SentryJsBridge.replayIntegration(js_util.jsify({ - 'maskAllText': options.experimental.replay.redactAllText, - // todo: is redactAllImages the same? - 'blockAllMedia': options.experimental.replay.redactAllImages, - })), - SentryJsBridge.replayCanvasIntegration(), - ], - }; - - // Remove null values to avoid unnecessary properties in the JS object - config.removeWhere((key, value) => value == null); - - SentryJsBridge.init(js_util.jsify(config)); - - return Future.value(); + return tryCatchAsync('init', () async { + await _loadSentryScripts(options); + + if (!_scriptLoaded) { + options.logger(SentryLevel.warning, + 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); + } + + final Map config = { + 'dsn': options.dsn, + 'debug': options.debug, + 'environment': options.environment, + 'release': options.release, + 'dist': options.dist, + 'autoSessionTracking': options.enableAutoSessionTracking, + 'attachStacktrace': options.attachStacktrace, + 'maxBreadcrumbs': options.maxBreadcrumbs, + 'replaysSessionSampleRate': + options.experimental.replay.sessionSampleRate, + 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, + // using defaultIntegrations ensures the we can control which integrations are added + 'defaultIntegrations': [ + SentryJsBridge.replayIntegration({ + 'maskAllText': options.experimental.replay.redactAllText, + // todo: is redactAllImages the same as blockAllMedia? + 'blockAllMedia': options.experimental.replay.redactAllImages, + }.jsify()), + SentryJsBridge.replayCanvasIntegration(), + ], + }; + + // Remove null values to avoid unnecessary properties in the JS object + config.removeWhere((key, value) => value == null); + + SentryJsBridge.init(config.jsify()); + + SpotlightBridge.init(); + }); } @override - Future captureEvent(SentryEvent event) { - SentryJsBridge.captureEvent(js_util.jsify(event.toJson())); - - return Future.value(); + Future captureEvent(SentryEvent event) async { + tryCatchSync('captureEvent', () { + SentryJsBridge.captureEvent(event.toJson().jsify()); + }); } @override Future captureEnvelope(SentryEnvelope envelope) async { - SentryJsBridge.getClient().sendEnvelope(js_util.jsify([ - envelope.header.toJson(), - [ - envelope.items.map((item) { - return [ - item.header.toJson(), - item.originalObject, - ]; - }).toList(), - ] - ])); - - return Future.value(); + tryCatchSync('captureEnvelope', () { + SentryJsBridge.getClient().sendEnvelope([ + envelope.header.toJson(), + [ + envelope.items.map((item) { + return [ + item.header.toJson(), + item.originalObject, + ]; + }).toList(), + ] + ].jsify()); + }); } @override - Future close() { - SentryJsBridge.close(); - - return Future.value(); + Future close() async { + tryCatchSync('close', () { + SentryJsBridge.close(); + }); } } bool _scriptLoaded = false; -Future _loadSentryScripts(SentryFlutterOptions options) async { +Future _loadSentryScripts(SentryFlutterOptions options, + {bool useIntegrity = true}) async { if (_scriptLoaded) return; - List scriptUrls = [ - 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js', - 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js' + final 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' + }, + { + // todo: fix double events from spotlight + 'url': + 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', + }, ]; - Map integrityHashes = { - 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.min.js': - 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL', - 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.min.js': - 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' - }; - - List> loadFutures = []; - - for (String url in scriptUrls) { - loadFutures.add(_loadScript(url, integrityHashes[url]!)); - } - try { - await Future.wait(loadFutures); + await Future.wait(scripts.map((script) => _loadScript( + script['url']!, useIntegrity ? script['integrity'] : null))); _scriptLoaded = true; - print('All Sentry scripts loaded successfully'); + 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) { - Completer completer = Completer(); - ScriptElement script = ScriptElement() +Future _loadScript(String src, String? integrity) { + final completer = Completer(); + final script = ScriptElement() ..src = src - ..integrity = integrity ..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/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/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({ From aad15e38cf1836f2200d49c5ffb2d6d0c6f130ad Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:01:10 +0200 Subject: [PATCH 04/15] update --- .../lib/src/native/sentry_native_invoker.dart | 1 + .../src/transport/file_system_transport.dart | 1 + flutter/lib/src/web/sentry_web_interop.dart | 67 ++++++++++--------- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/flutter/lib/src/native/sentry_native_invoker.dart b/flutter/lib/src/native/sentry_native_invoker.dart index dbda8c4513..8a06cc0be0 100644 --- a/flutter/lib/src/native/sentry_native_invoker.dart +++ b/flutter/lib/src/native/sentry_native_invoker.dart @@ -27,6 +27,7 @@ mixin SentryNativeSafeInvoker { try { return fn(); } catch (error, stackTrace) { + print(stackTrace); _logError(nativeMethodName, error, stackTrace); // ignore: invalid_use_of_internal_member if (options.automatedTestMode) { diff --git a/flutter/lib/src/transport/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart index 8faad0822d..79eb4f214d 100644 --- a/flutter/lib/src/transport/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -49,6 +49,7 @@ class EventTransportAdapter implements Transport { if (item.header.type == 'event' && object is SentryEvent) { return _eventTransport.sendEvent(object); } else { + print('Sending envelope'); return _envelopeTransport.send(envelope); } } diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index 4dbfe00322..583235c623 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -44,12 +44,12 @@ class SentryWebInterop 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, // using defaultIntegrations ensures the we can control which integrations are added 'defaultIntegrations': [ - SentryJsBridge.replayIntegration({ - 'maskAllText': options.experimental.replay.redactAllText, - // todo: is redactAllImages the same as blockAllMedia? - 'blockAllMedia': options.experimental.replay.redactAllImages, - }.jsify()), - SentryJsBridge.replayCanvasIntegration(), + // SentryJsBridge.replayIntegration({ + // 'maskAllText': options.experimental.replay.redactAllText, + // // todo: is redactAllImages the same as blockAllMedia? + // 'blockAllMedia': options.experimental.replay.redactAllImages, + // }.jsify()), + // SentryJsBridge.replayCanvasIntegration(), ], }; @@ -58,7 +58,7 @@ class SentryWebInterop SentryJsBridge.init(config.jsify()); - SpotlightBridge.init(); + // SpotlightBridge.init(); }); } @@ -71,18 +71,20 @@ class SentryWebInterop @override Future captureEnvelope(SentryEnvelope envelope) async { - tryCatchSync('captureEnvelope', () { - SentryJsBridge.getClient().sendEnvelope([ - envelope.header.toJson(), - [ - envelope.items.map((item) { - return [ - item.header.toJson(), - item.originalObject, - ]; - }).toList(), - ] - ].jsify()); + await tryCatchAsync('captureEnvelope', () async { + final List jsItems = []; + + for (final item in envelope.items) { + // todo: add support for different type of items + final jsItem = [ + (await item.header.toJson()).jsify(), + (item.originalObject as SentryTransaction).toJson().jsify() + ]; + jsItems.add(jsItem); + } + + SentryJsBridge.getClient() + .sendEnvelope([envelope.header.toJson().jsify(), jsItems].jsify()); }); } @@ -102,21 +104,20 @@ Future _loadSentryScripts(SentryFlutterOptions options, final 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' - }, - { - // todo: fix double events from spotlight - 'url': - 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', + 'url': 'http://localhost:3000/local_bundle.js', + // 'integrity': + // 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' }, + // { + // 'url': 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.js', + // 'integrity': + // 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' + // }, + // { + // // todo: fix double events from spotlight + // 'url': + // 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', + // }, ]; try { From 6e623a152c2c8a00f0fad07eebc7f1a8b9afca48 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:02:29 +0200 Subject: [PATCH 05/15] update --- dart/lib/src/sentry_client.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index bcdc9b68a8..c4ebac3db5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -187,7 +187,7 @@ class SentryClient { ); final id = await captureEnvelope(envelope); - return SentryId.empty(); + return id ?? SentryId.empty(); } bool _isIgnoredError(SentryEvent event) { From e5da3b6452be9a47b713d55eddccbb8e264922fa Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:03:21 +0200 Subject: [PATCH 06/15] Update --- flutter/lib/src/native/sentry_native_invoker.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/lib/src/native/sentry_native_invoker.dart b/flutter/lib/src/native/sentry_native_invoker.dart index 8a06cc0be0..dbda8c4513 100644 --- a/flutter/lib/src/native/sentry_native_invoker.dart +++ b/flutter/lib/src/native/sentry_native_invoker.dart @@ -27,7 +27,6 @@ mixin SentryNativeSafeInvoker { try { return fn(); } catch (error, stackTrace) { - print(stackTrace); _logError(nativeMethodName, error, stackTrace); // ignore: invalid_use_of_internal_member if (options.automatedTestMode) { From a8697e5dc3286c66e3dfb922c79666896c6e5e49 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:10:44 +0200 Subject: [PATCH 07/15] Update cdn --- flutter/lib/src/web/sentry_web_interop.dart | 34 +++++++++------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index 583235c623..aa4a578095 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -44,12 +44,12 @@ class SentryWebInterop 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, // using defaultIntegrations ensures the we can control which integrations are added 'defaultIntegrations': [ - // SentryJsBridge.replayIntegration({ - // 'maskAllText': options.experimental.replay.redactAllText, - // // todo: is redactAllImages the same as blockAllMedia? - // 'blockAllMedia': options.experimental.replay.redactAllImages, - // }.jsify()), - // SentryJsBridge.replayCanvasIntegration(), + SentryJsBridge.replayIntegration({ + 'maskAllText': options.experimental.replay.redactAllText, + // todo: is redactAllImages the same as blockAllMedia? + 'blockAllMedia': options.experimental.replay.redactAllImages, + }.jsify()), + SentryJsBridge.replayCanvasIntegration(), ], }; @@ -104,20 +104,16 @@ Future _loadSentryScripts(SentryFlutterOptions options, final scripts = [ { - 'url': 'http://localhost:3000/local_bundle.js', - // 'integrity': - // 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' + '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' }, - // { - // 'url': 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.js', - // 'integrity': - // 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' - // }, - // { - // // todo: fix double events from spotlight - // 'url': - // 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', - // }, ]; try { From 2a07c1a6c99872fabbeb7f574ec7ab292782f828 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:13:20 +0200 Subject: [PATCH 08/15] add comment --- flutter/lib/src/web/sentry_web_interop.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index aa4a578095..a871068a5b 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -71,7 +71,7 @@ class SentryWebInterop @override Future captureEnvelope(SentryEnvelope envelope) async { - await tryCatchAsync('captureEnvelope', () async { + return tryCatchAsync('captureEnvelope', () async { final List jsItems = []; for (final item in envelope.items) { @@ -90,7 +90,7 @@ class SentryWebInterop @override Future close() async { - tryCatchSync('close', () { + return tryCatchSync('close', () { SentryJsBridge.close(); }); } @@ -102,6 +102,7 @@ Future _loadSentryScripts(SentryFlutterOptions options, {bool useIntegrity = true}) async { if (_scriptLoaded) return; + // todo: put this somewhere else so we can auto-update it as well final scripts = [ { 'url': From 691db8673f909c77935d7828d9b43c93cb12b112 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 19 Aug 2024 22:19:55 +0200 Subject: [PATCH 09/15] split impl --- flutter/lib/src/sentry_flutter.dart | 8 ++++---- flutter/lib/src/web/factory.dart | 3 +++ flutter/lib/src/web/factory_noop.dart | 6 ++++++ flutter/lib/src/web/factory_web.dart | 7 +++++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 flutter/lib/src/web/factory.dart create mode 100644 flutter/lib/src/web/factory_noop.dart create mode 100644 flutter/lib/src/web/factory_web.dart diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index cb16abed5e..6a9f4c07f5 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -18,7 +18,8 @@ import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/integrations.dart'; import 'integrations/screenshot_integration.dart'; import 'integrations/web_sdk_integration.dart'; -import 'native/factory.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'; @@ -28,7 +29,6 @@ import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; import 'web/sentry_web_binding.dart'; -import 'web/sentry_web_interop.dart'; /// Configuration options callback typedef FlutterOptionsConfiguration = FutureOr Function( @@ -71,11 +71,11 @@ mixin SentryFlutter { } if (flutterOptions.platformChecker.hasNativeIntegration) { - _native = createBinding(flutterOptions); + _native = nativeFactory.createBinding(flutterOptions); } if (flutterOptions.platformChecker.isWeb) { - _webBinding = SentryWebInterop(flutterOptions); + _webBinding = webFactory.createBinding(flutterOptions); } final platformDispatcher = PlatformDispatcher.instance; 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..95e6d1df18 --- /dev/null +++ b/flutter/lib/src/web/factory_noop.dart @@ -0,0 +1,6 @@ +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; + +SentryWebBinding createBinding(SentryFlutterOptions options) { + throw UnsupportedError("Web binding is not supported on this platform."); +} diff --git a/flutter/lib/src/web/factory_web.dart b/flutter/lib/src/web/factory_web.dart new file mode 100644 index 0000000000..294ba1a842 --- /dev/null +++ b/flutter/lib/src/web/factory_web.dart @@ -0,0 +1,7 @@ +import '../../sentry_flutter.dart'; +import '../web/sentry_web_binding.dart'; +import 'sentry_web_interop.dart'; + +SentryWebBinding createBinding(SentryFlutterOptions options) { + return SentryWebInterop(options); +} From 34816592273c65a0973cbaa227344d79ffd0b92d Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 20 Aug 2024 13:54:58 +0200 Subject: [PATCH 10/15] Update --- flutter/lib/src/web/factory_noop.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/web/factory_noop.dart b/flutter/lib/src/web/factory_noop.dart index 95e6d1df18..3a703d13bd 100644 --- a/flutter/lib/src/web/factory_noop.dart +++ b/flutter/lib/src/web/factory_noop.dart @@ -1,6 +1,19 @@ import '../../sentry_flutter.dart'; import '../web/sentry_web_binding.dart'; -SentryWebBinding createBinding(SentryFlutterOptions options) { - throw UnsupportedError("Web binding is not supported on this platform."); +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 {} } + +SentryWebBinding createBinding(SentryFlutterOptions options) => + NoOpWebInterop(); From 1d5c17dab20cff08dc90b8b614850a1ae98282c3 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Tue, 20 Aug 2024 16:01:21 +0200 Subject: [PATCH 11/15] Use envelopes for everything --- flutter/example/lib/main.dart | 12 ++- .../src/integrations/web_sdk_integration.dart | 1 + flutter/lib/src/sentry_flutter.dart | 13 +++- .../src/transport/file_system_transport.dart | 22 ------ .../src/transport/javascript_transport.dart | 13 ---- flutter/lib/src/web/sentry_js_bridge.dart | 20 +++++ flutter/lib/src/web/sentry_web_binding.dart | 6 ++ flutter/lib/src/web/sentry_web_interop.dart | 74 +++++++++++++------ .../lib/src/web_replay_event_processor.dart | 34 +++++++++ 9 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 flutter/lib/src/web_replay_event_processor.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 257921740e..716203bd9c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -91,8 +91,8 @@ Future setupSentry( options.maxResponseBodySize = MaxResponseBodySize.always; options.navigatorKey = navigatorKey; - options.experimental.replay.sessionSampleRate = 1.0; - options.experimental.replay.errorSampleRate = 1.0; + options.experimental.replay.sessionSampleRate = 0; + options.experimental.replay.errorSampleRate = 0; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { @@ -778,11 +778,9 @@ void navigateToAutoCloseScreen(BuildContext context) { Future tryCatch() async { try { - // Some code that might throw - throw Exception('Test exception'); - } catch (e, stackTrace) { - Sentry.captureException(e, stackTrace: stackTrace); - // SentryJS.captureException(e); + throw StateError('try catch'); + } catch (error, stackTrace) { + await Sentry.captureException(error, stackTrace: stackTrace); } } diff --git a/flutter/lib/src/integrations/web_sdk_integration.dart b/flutter/lib/src/integrations/web_sdk_integration.dart index 2b16a46ae2..f833868050 100644 --- a/flutter/lib/src/integrations/web_sdk_integration.dart +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -16,6 +16,7 @@ class WebSdkIntegration implements Integration { try { _webBinding.init(options); + options.sdk.addIntegration('WebSdkIntegration'); } catch (exception, stackTrace) { options.logger( diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 6a9f4c07f5..afdecdcf9b 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -29,6 +29,7 @@ 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( @@ -74,6 +75,7 @@ mixin SentryFlutter { _native = nativeFactory.createBinding(flutterOptions); } + // todo: maybe makes sense to combine the bindings into a single interface if (flutterOptions.platformChecker.isWeb) { _webBinding = webFactory.createBinding(flutterOptions); } @@ -139,10 +141,10 @@ mixin SentryFlutter { } if (options.platformChecker.isWeb) { - final eventTransport = JavascriptEventTransport(_webBinding!); - final envelopeTransport = JavascriptEnvelopeTransport(_webBinding!); - options.transport = - EventTransportAdapter(eventTransport, envelopeTransport); + options.transport = JavascriptEnvelopeTransport(_webBinding!); + + // todo: only if replay enabled + options.addEventProcessor(WebReplayEventProcessor(_webBinding!)); } options.addEventProcessor(FlutterEnricherEventProcessor(options)); @@ -298,5 +300,8 @@ mixin SentryFlutter { static SentryNativeBinding? _native; + @internal + static SentryWebBinding? get webBinding => _webBinding; + static SentryWebBinding? _webBinding; } diff --git a/flutter/lib/src/transport/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart index 79eb4f214d..f77266923f 100644 --- a/flutter/lib/src/transport/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -35,25 +35,3 @@ class FileSystemTransport implements Transport { return envelope.header.eventId; } } - -class EventTransportAdapter implements Transport { - final EventTransport _eventTransport; - final Transport _envelopeTransport; - - EventTransportAdapter(this._eventTransport, this._envelopeTransport); - - @override - Future send(SentryEnvelope envelope) async { - for (final item in envelope.items) { - final object = item.originalObject; - if (item.header.type == 'event' && object is SentryEvent) { - return _eventTransport.sendEvent(object); - } else { - print('Sending envelope'); - return _envelopeTransport.send(envelope); - } - } - // If no event is found in the envelope, return an empty ID - return SentryId.empty(); - } -} diff --git a/flutter/lib/src/transport/javascript_transport.dart b/flutter/lib/src/transport/javascript_transport.dart index 107eec1b65..ade4538f6d 100644 --- a/flutter/lib/src/transport/javascript_transport.dart +++ b/flutter/lib/src/transport/javascript_transport.dart @@ -1,19 +1,6 @@ import '../../sentry_flutter.dart'; import '../web/sentry_web_binding.dart'; -class JavascriptEventTransport implements EventTransport { - final SentryWebBinding _binding; - - JavascriptEventTransport(this._binding); - - @override - Future sendEvent(SentryEvent event) { - _binding.captureEvent(event); - - return Future.value(event.eventId); - } -} - class JavascriptEnvelopeTransport implements Transport { final SentryWebBinding _binding; diff --git a/flutter/lib/src/web/sentry_js_bridge.dart b/flutter/lib/src/web/sentry_js_bridge.dart index 77ed9710d0..006eb8baad 100644 --- a/flutter/lib/src/web/sentry_js_bridge.dart +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -1,6 +1,8 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; +import '../sentry_replay_options.dart'; + @internal @JS('Spotlight') @staticInterop @@ -25,6 +27,24 @@ class SentryJsBridge { external static JSAny? replayCanvasIntegration(); external static _SentryJsClient getClient(); + + external static JSAny? getReplay(); +} + +@JS('Replay') +@staticInterop +class _SentryReplay {} + +extension SentryReplayExtension on JSAny? { + external void start(); + + external void startBuffering(); + + external void stop(); + + external void flush(); + + external JSString getReplayId(); } @JS('Client') diff --git a/flutter/lib/src/web/sentry_web_binding.dart b/flutter/lib/src/web/sentry_web_binding.dart index 349a91e084..cd9fcda1e9 100644 --- a/flutter/lib/src/web/sentry_web_binding.dart +++ b/flutter/lib/src/web/sentry_web_binding.dart @@ -11,5 +11,11 @@ abstract class SentryWebBinding { Future captureEvent(SentryEvent event); + Future flushReplay(); + + Future startReplay(); + + Future getReplayId(); + Future close(); } diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index a871068a5b..57e810a62b 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -20,6 +20,8 @@ class SentryWebInterop SentryWebInterop(this._options); + dynamic replay; + @override Future init(SentryFlutterOptions options) async { return tryCatchAsync('init', () async { @@ -30,6 +32,12 @@ class SentryWebInterop 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); } + replay = SentryJsBridge.replayIntegration({ + 'maskAllText': options.experimental.replay.redactAllText, + // todo: is redactAllImages the same as blockAllMedia? + 'blockAllMedia': options.experimental.replay.redactAllImages, + }.jsify()); + final Map config = { 'dsn': options.dsn, 'debug': options.debug, @@ -39,16 +47,11 @@ class SentryWebInterop 'autoSessionTracking': options.enableAutoSessionTracking, 'attachStacktrace': options.attachStacktrace, 'maxBreadcrumbs': options.maxBreadcrumbs, - 'replaysSessionSampleRate': - options.experimental.replay.sessionSampleRate, - 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, + 'replaysSessionSampleRate': 0, + 'replaysOnErrorSampleRate': 0, // using defaultIntegrations ensures the we can control which integrations are added 'defaultIntegrations': [ - SentryJsBridge.replayIntegration({ - 'maskAllText': options.experimental.replay.redactAllText, - // todo: is redactAllImages the same as blockAllMedia? - 'blockAllMedia': options.experimental.replay.redactAllImages, - }.jsify()), + replay, SentryJsBridge.replayCanvasIntegration(), ], }; @@ -58,13 +61,14 @@ class SentryWebInterop SentryJsBridge.init(config.jsify()); - // SpotlightBridge.init(); + await startReplay(); }); } @override Future captureEvent(SentryEvent event) async { tryCatchSync('captureEvent', () { + print(event.toJson()); SentryJsBridge.captureEvent(event.toJson().jsify()); }); } @@ -76,15 +80,24 @@ class SentryWebInterop for (final item in envelope.items) { // todo: add support for different type of items - final jsItem = [ - (await item.header.toJson()).jsify(), - (item.originalObject as SentryTransaction).toJson().jsify() - ]; + // maybe add a generic to sentryenvelope? + final originalObject = item.originalObject; + final List jsItem = [(await item.header.toJson())]; + if (originalObject is SentryTransaction) { + jsItem.add(originalObject.toJson().jsify()); + } + if (originalObject is SentryEvent) { + jsItem.add(originalObject.toJson().jsify()); + } + if (originalObject is SentryAttachment) { + jsItem.add(await originalObject.bytes); + } jsItems.add(jsItem); } - SentryJsBridge.getClient() - .sendEnvelope([envelope.header.toJson().jsify(), jsItems].jsify()); + final jsEnvelope = [envelope.header.toJson(), jsItems].jsify(); + + SentryJsBridge.getClient().sendEnvelope(jsEnvelope); }); } @@ -94,6 +107,22 @@ class SentryWebInterop SentryJsBridge.close(); }); } + + @override + Future startReplay() async { + replay.startBuffering(); + } + + @override + Future flushReplay() async { + replay.flush(); + } + + @override + Future getReplayId() async { + final sentryIdString = replay.getReplayId() as String; + return SentryId.fromId(sentryIdString); + } } bool _scriptLoaded = false; @@ -102,18 +131,17 @@ Future _loadSentryScripts(SentryFlutterOptions options, {bool useIntegrity = true}) async { if (_scriptLoaded) return; - // todo: put this somewhere else so we can auto-update it as well + // todo: put this somewhere else so we can auto-update it as well and only enable non minified bundles in dev mode final 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/bundle.tracing.replay.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' + 'url': 'https://browser.sentry-cdn.com/8.24.0/replay-canvas.js', + // 'integrity': + // 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' }, ]; 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..fd6ad9515a --- /dev/null +++ b/flutter/lib/src/web_replay_event_processor.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import '../sentry_flutter.dart'; +import 'web/sentry_web_binding.dart'; + +class WebReplayEventProcessor implements EventProcessor { + WebReplayEventProcessor(this._binding); + + final SentryWebBinding _binding; + + @override + FutureOr apply(SentryEvent event, Hint hint) async { + try { + await _binding.flushReplay(); + + await Future.delayed(Duration(seconds: 1)); + + final sentryId = await _binding.getReplayId(); + + print(sentryId); + event = event.copyWith(tags: { + ...?event.tags, + 'replayId': sentryId.toString(), + }); + event.tags?.forEach((key, value) { + print('$key: $value'); + }); + } catch (exception, stackTrace) { + print('Failed to get replay id: $exception $stackTrace'); + } + + return event; + } +} From 5612ea63b3e34bc2f01ed5e2587b9eab5356dd66 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Wed, 21 Aug 2024 17:10:08 +0200 Subject: [PATCH 12/15] Fix replays and sessions --- flutter/example/lib/main.dart | 7 +-- flutter/lib/src/sentry_flutter.dart | 4 +- .../src/transport/file_system_transport.dart | 1 + flutter/lib/src/web/factory_noop.dart | 11 +++++ flutter/lib/src/web/sentry_js_bridge.dart | 30 ++++++++++-- flutter/lib/src/web/sentry_web_binding.dart | 2 - flutter/lib/src/web/sentry_web_interop.dart | 47 +++++++++---------- .../lib/src/web_replay_event_processor.dart | 20 ++++---- 8 files changed, 79 insertions(+), 43 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 716203bd9c..27324e7055 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -83,16 +83,17 @@ 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.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 = 0; - options.experimental.replay.errorSampleRate = 0; + options.experimental.replay.sessionSampleRate = 0.5; + options.experimental.replay.errorSampleRate = 1; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index afdecdcf9b..7c747f3840 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -143,8 +143,8 @@ mixin SentryFlutter { if (options.platformChecker.isWeb) { options.transport = JavascriptEnvelopeTransport(_webBinding!); - // todo: only if replay enabled - options.addEventProcessor(WebReplayEventProcessor(_webBinding!)); + options.addEventProcessor( + WebReplayEventProcessor(_webBinding!, options.experimental.replay)); } options.addEventProcessor(FlutterEnricherEventProcessor(options)); diff --git a/flutter/lib/src/transport/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart index f77266923f..8175914e46 100644 --- a/flutter/lib/src/transport/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -1,5 +1,6 @@ // backcompatibility for Flutter < 3.3 // ignore: unnecessary_import +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; diff --git a/flutter/lib/src/web/factory_noop.dart b/flutter/lib/src/web/factory_noop.dart index 3a703d13bd..42c20873c3 100644 --- a/flutter/lib/src/web/factory_noop.dart +++ b/flutter/lib/src/web/factory_noop.dart @@ -13,6 +13,17 @@ class NoOpWebInterop implements SentryWebBinding { @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) => diff --git a/flutter/lib/src/web/sentry_js_bridge.dart b/flutter/lib/src/web/sentry_js_bridge.dart index 006eb8baad..046d8dc7ef 100644 --- a/flutter/lib/src/web/sentry_js_bridge.dart +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -29,11 +29,35 @@ class SentryJsBridge { external static _SentryJsClient getClient(); external static JSAny? getReplay(); + + external static void captureSession(); + + external static _Scope? getCurrentScope(); + + external static _Scope? getIsolationScope(); + + static SentryJsSession? getSession() { + return getCurrentScope()?.getSession() ?? getIsolationScope()?.getSession(); + } } -@JS('Replay') +@JS('Session') @staticInterop -class _SentryReplay {} +class SentryJsSession {} + +extension SentryJsSessionExtension on SentryJsSession { + external JSString status; + + external JSNumber errors; +} + +@JS('Scope') +@staticInterop +class _Scope {} + +extension SentryScopeExtension on _Scope { + external SentryJsSession? getSession(); +} extension SentryReplayExtension on JSAny? { external void start(); @@ -44,7 +68,7 @@ extension SentryReplayExtension on JSAny? { external void flush(); - external JSString getReplayId(); + external JSString? getReplayId(); } @JS('Client') diff --git a/flutter/lib/src/web/sentry_web_binding.dart b/flutter/lib/src/web/sentry_web_binding.dart index cd9fcda1e9..259fafc8d3 100644 --- a/flutter/lib/src/web/sentry_web_binding.dart +++ b/flutter/lib/src/web/sentry_web_binding.dart @@ -13,8 +13,6 @@ abstract class SentryWebBinding { Future flushReplay(); - Future startReplay(); - Future getReplayId(); Future close(); diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index 57e810a62b..442a2ee123 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -47,8 +47,9 @@ class SentryWebInterop 'autoSessionTracking': options.enableAutoSessionTracking, 'attachStacktrace': options.attachStacktrace, 'maxBreadcrumbs': options.maxBreadcrumbs, - 'replaysSessionSampleRate': 0, - 'replaysOnErrorSampleRate': 0, + 'replaysSessionSampleRate': + options.experimental.replay.sessionSampleRate, + 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, // using defaultIntegrations ensures the we can control which integrations are added 'defaultIntegrations': [ replay, @@ -61,17 +62,14 @@ class SentryWebInterop SentryJsBridge.init(config.jsify()); - await startReplay(); + // SpotlightBridge.init(); + + // await startReplay(); }); } @override - Future captureEvent(SentryEvent event) async { - tryCatchSync('captureEvent', () { - print(event.toJson()); - SentryJsBridge.captureEvent(event.toJson().jsify()); - }); - } + Future captureEvent(SentryEvent event) async {} @override Future captureEnvelope(SentryEnvelope envelope) async { @@ -79,20 +77,20 @@ class SentryWebInterop final List jsItems = []; for (final item in envelope.items) { - // todo: add support for different type of items - // maybe add a generic to sentryenvelope? final originalObject = item.originalObject; - final List jsItem = [(await item.header.toJson())]; - if (originalObject is SentryTransaction) { - jsItem.add(originalObject.toJson().jsify()); - } + jsItems.add([ + (await item.header.toJson()), + (await originalObject?.getPayload()) + ]); + if (originalObject is SentryEvent) { - jsItem.add(originalObject.toJson().jsify()); + final session = SentryJsBridge.getSession(); + if (envelope.containsUnhandledException) { + session?.status = 'crashed'.toJS; + } + session?.errors = originalObject.exceptions?.length.toJS ?? 0.toJS; + SentryJsBridge.captureSession(); } - if (originalObject is SentryAttachment) { - jsItem.add(await originalObject.bytes); - } - jsItems.add(jsItem); } final jsEnvelope = [envelope.header.toJson(), jsItems].jsify(); @@ -108,11 +106,6 @@ class SentryWebInterop }); } - @override - Future startReplay() async { - replay.startBuffering(); - } - @override Future flushReplay() async { replay.flush(); @@ -133,6 +126,10 @@ Future _loadSentryScripts(SentryFlutterOptions options, // todo: put this somewhere else so we can auto-update it as well and only enable non minified bundles in dev mode final scripts = [ + // { + // 'url': + // 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', + // }, { 'url': 'https://browser.sentry-cdn.com/8.24.0/bundle.tracing.replay.js', // 'integrity': diff --git a/flutter/lib/src/web_replay_event_processor.dart b/flutter/lib/src/web_replay_event_processor.dart index fd6ad9515a..99b7eb5612 100644 --- a/flutter/lib/src/web_replay_event_processor.dart +++ b/flutter/lib/src/web_replay_event_processor.dart @@ -1,32 +1,36 @@ import 'dart:async'; import '../sentry_flutter.dart'; +import 'sentry_replay_options.dart'; import 'web/sentry_web_binding.dart'; class WebReplayEventProcessor implements EventProcessor { - WebReplayEventProcessor(this._binding); + WebReplayEventProcessor(this._binding, this._replayOptions); final SentryWebBinding _binding; + final SentryReplayOptions _replayOptions; + bool hasFlushedReplay = false; @override FutureOr apply(SentryEvent event, Hint hint) async { try { - await _binding.flushReplay(); + if (!_replayOptions.isEnabled) { + return event; + } - await Future.delayed(Duration(seconds: 1)); + if (event.exceptions?.isNotEmpty == true && !hasFlushedReplay) { + await _binding.flushReplay(); + hasFlushedReplay = true; + } final sentryId = await _binding.getReplayId(); - print(sentryId); event = event.copyWith(tags: { ...?event.tags, 'replayId': sentryId.toString(), }); - event.tags?.forEach((key, value) { - print('$key: $value'); - }); } catch (exception, stackTrace) { - print('Failed to get replay id: $exception $stackTrace'); + // todo: log } return event; From f757d37623422a4266397afea07511584cba0301 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Thu, 22 Aug 2024 16:26:38 +0200 Subject: [PATCH 13/15] Updat --- flutter/example/lib/main.dart | 2 +- .../src/integrations/web_sdk_integration.dart | 4 +- flutter/lib/src/sentry_flutter.dart | 6 +- .../src/transport/file_system_transport.dart | 1 - .../src/transport/javascript_transport.dart | 19 +- flutter/lib/src/web/factory_web.dart | 7 +- flutter/lib/src/web/sentry_js_bridge.dart | 96 ++-- flutter/lib/src/web/sentry_web_binding.dart | 4 +- flutter/lib/src/web/sentry_web_interop.dart | 96 ++-- .../lib/src/web_replay_event_processor.dart | 27 +- .../test/integrations/init_web_sdk_test.dart | 85 ++++ flutter/test/mocks.dart | 4 +- flutter/test/mocks.mocks.dart | 468 +++++++++++------- 13 files changed, 549 insertions(+), 270 deletions(-) create mode 100644 flutter/test/integrations/init_web_sdk_test.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 27324e7055..af2979657c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -92,7 +92,7 @@ Future setupSentry( options.maxResponseBodySize = MaxResponseBodySize.always; options.navigatorKey = navigatorKey; - options.experimental.replay.sessionSampleRate = 0.5; + options.experimental.replay.sessionSampleRate = 1; options.experimental.replay.errorSampleRate = 1; _isIntegrationTest = isIntegrationTest; diff --git a/flutter/lib/src/integrations/web_sdk_integration.dart b/flutter/lib/src/integrations/web_sdk_integration.dart index f833868050..0628337ac7 100644 --- a/flutter/lib/src/integrations/web_sdk_integration.dart +++ b/flutter/lib/src/integrations/web_sdk_integration.dart @@ -5,11 +5,11 @@ 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; - WebSdkIntegration(this._webBinding); - @override FutureOr call(Hub hub, SentryFlutterOptions options) { _options = options; diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 7c747f3840..7cb028b6d0 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -141,10 +141,8 @@ mixin SentryFlutter { } if (options.platformChecker.isWeb) { - options.transport = JavascriptEnvelopeTransport(_webBinding!); - - options.addEventProcessor( - WebReplayEventProcessor(_webBinding!, options.experimental.replay)); + options.transport = JavascriptEnvelopeTransport(_webBinding!, options); + options.addEventProcessor(WebReplayEventProcessor(_webBinding!, options)); } options.addEventProcessor(FlutterEnricherEventProcessor(options)); diff --git a/flutter/lib/src/transport/file_system_transport.dart b/flutter/lib/src/transport/file_system_transport.dart index 8175914e46..f77266923f 100644 --- a/flutter/lib/src/transport/file_system_transport.dart +++ b/flutter/lib/src/transport/file_system_transport.dart @@ -1,6 +1,5 @@ // backcompatibility for Flutter < 3.3 // ignore: unnecessary_import -import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; diff --git a/flutter/lib/src/transport/javascript_transport.dart b/flutter/lib/src/transport/javascript_transport.dart index ade4538f6d..a2718946a8 100644 --- a/flutter/lib/src/transport/javascript_transport.dart +++ b/flutter/lib/src/transport/javascript_transport.dart @@ -2,14 +2,25 @@ import '../../sentry_flutter.dart'; import '../web/sentry_web_binding.dart'; class JavascriptEnvelopeTransport implements Transport { - final SentryWebBinding _binding; + JavascriptEnvelopeTransport(this._binding, this._options); - JavascriptEnvelopeTransport(this._binding); + final SentryFlutterOptions _options; + final SentryWebBinding _binding; @override Future send(SentryEnvelope envelope) { - _binding.captureEnvelope(envelope); + try { + _binding.captureEnvelope(envelope); - return Future.value(SentryId.empty()); + 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_web.dart b/flutter/lib/src/web/factory_web.dart index 294ba1a842..dea9cd3e70 100644 --- a/flutter/lib/src/web/factory_web.dart +++ b/flutter/lib/src/web/factory_web.dart @@ -1,7 +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) { - return SentryWebInterop(options); +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 index 046d8dc7ef..d25e286a74 100644 --- a/flutter/lib/src/web/sentry_js_bridge.dart +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -1,13 +1,47 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; -import '../sentry_replay_options.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(); +} -@internal -@JS('Spotlight') -@staticInterop -class SpotlightBridge { - external static void init(); +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 @@ -18,29 +52,43 @@ class SentryJsBridge { external static void close(); - external static JSAny? captureMessage(JSString message); - - external static JSString captureEvent(JSAny? event); - - external static JSAny? replayIntegration(JSAny? configuration); + external static SentryJsReplay replayIntegration(JSAny? configuration); external static JSAny? replayCanvasIntegration(); - external static _SentryJsClient getClient(); + external static SentryJsClient getClient(); external static JSAny? getReplay(); external static void captureSession(); - external static _Scope? getCurrentScope(); + external static JSAny? browserTracingIntegration(); - external static _Scope? getIsolationScope(); + 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 {} @@ -53,28 +101,16 @@ extension SentryJsSessionExtension on SentryJsSession { @JS('Scope') @staticInterop -class _Scope {} +class SentryJsScope {} -extension SentryScopeExtension on _Scope { +extension SentryScopeExtension on SentryJsScope { external SentryJsSession? getSession(); } -extension SentryReplayExtension on JSAny? { - external void start(); - - external void startBuffering(); - - external void stop(); - - external void flush(); - - external JSString? getReplayId(); -} - @JS('Client') @staticInterop -class _SentryJsClient {} +class SentryJsClient {} -extension SentryJsClientExtension on _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 index 259fafc8d3..e85175e779 100644 --- a/flutter/lib/src/web/sentry_web_binding.dart +++ b/flutter/lib/src/web/sentry_web_binding.dart @@ -9,11 +9,9 @@ abstract class SentryWebBinding { Future captureEnvelope(SentryEnvelope envelope); - Future captureEvent(SentryEvent event); - Future flushReplay(); - Future getReplayId(); + Future getReplayId(); Future close(); } diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index 442a2ee123..890de0f4b5 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -2,10 +2,11 @@ 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 'sentry_js_bridge.dart'; import 'dart:html'; +import 'dart:js_util' as js_util; import 'sentry_web_binding.dart'; @@ -14,13 +15,14 @@ import 'sentry_web_binding.dart'; class SentryWebInterop with SentryNativeSafeInvoker implements SentryWebBinding { + SentryWebInterop(this._jsBridge, this._options); + @override SentryFlutterOptions get options => _options; final SentryFlutterOptions _options; + final SentryJsApi _jsBridge; - SentryWebInterop(this._options); - - dynamic replay; + SentryJsReplay? _replay; @override Future init(SentryFlutterOptions options) async { @@ -32,9 +34,8 @@ class SentryWebInterop 'Sentry scripts are not loaded, cannot initialize Sentry JS SDK.'); } - replay = SentryJsBridge.replayIntegration({ + _replay = _jsBridge.replayIntegration({ 'maskAllText': options.experimental.replay.redactAllText, - // todo: is redactAllImages the same as blockAllMedia? 'blockAllMedia': options.experimental.replay.redactAllImages, }.jsify()); @@ -44,6 +45,8 @@ class SentryWebInterop '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, @@ -52,25 +55,23 @@ class SentryWebInterop 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, // using defaultIntegrations ensures the we can control which integrations are added 'defaultIntegrations': [ - replay, - SentryJsBridge.replayCanvasIntegration(), + _replay, + _jsBridge.replayCanvasIntegration(), + // todo: check which default browser integrations make sense + // todo: test if the breadcrumbs make sense + _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); - SentryJsBridge.init(config.jsify()); - - // SpotlightBridge.init(); - - // await startReplay(); + _jsBridge.init(config.jsify()); }); } - @override - Future captureEvent(SentryEvent event) async {} - @override Future captureEnvelope(SentryEnvelope envelope) async { return tryCatchAsync('captureEnvelope', () async { @@ -83,65 +84,84 @@ class SentryWebInterop (await originalObject?.getPayload()) ]); - if (originalObject is SentryEvent) { - final session = SentryJsBridge.getSession(); + // 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; - SentryJsBridge.captureSession(); + _jsBridge.captureSession(); } } final jsEnvelope = [envelope.header.toJson(), jsItems].jsify(); - SentryJsBridge.getClient().sendEnvelope(jsEnvelope); + _jsBridge.getClient().sendEnvelope(jsEnvelope); }); } @override Future close() async { return tryCatchSync('close', () { - SentryJsBridge.close(); + _jsBridge.close(); }); } @override Future flushReplay() async { - replay.flush(); + return tryCatchAsync('flushReplay', () async { + if (_replay == null) { + return; + } + await js_util.promiseToFuture(_replay!.flush()); + }); } @override - Future getReplayId() async { - final sentryIdString = replay.getReplayId() as String; - return SentryId.fromId(sentryIdString); + 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 as well and only enable non minified bundles in dev mode - final scripts = [ - // { - // 'url': - // 'https://unpkg.com/@spotlightjs/overlay@latest/dist/sentry-spotlight.iife.js', - // }, + // 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.js', - // 'integrity': - // 'sha384-eEn/WSvcP5C2h5g0AGe5LCsheNNlNkn/iV8y5zOylmPoOfSyvZ23HBDnOhoB0sdL' + '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.js', - // 'integrity': - // 'sha384-gSFCG8IdZobb6PWs7SwuaES/R5PPt+gw4y6N/Kkwlic+1Hzf21EUm5Dg/WbYMxTE' + '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))); diff --git a/flutter/lib/src/web_replay_event_processor.dart b/flutter/lib/src/web_replay_event_processor.dart index 99b7eb5612..3abd108e19 100644 --- a/flutter/lib/src/web_replay_event_processor.dart +++ b/flutter/lib/src/web_replay_event_processor.dart @@ -1,38 +1,47 @@ import 'dart:async'; import '../sentry_flutter.dart'; -import 'sentry_replay_options.dart'; import 'web/sentry_web_binding.dart'; class WebReplayEventProcessor implements EventProcessor { - WebReplayEventProcessor(this._binding, this._replayOptions); + WebReplayEventProcessor(this._binding, this._options); final SentryWebBinding _binding; - final SentryReplayOptions _replayOptions; - bool hasFlushedReplay = false; + final SentryFlutterOptions _options; + bool _hasFlushedReplay = false; @override FutureOr apply(SentryEvent event, Hint hint) async { try { - if (!_replayOptions.isEnabled) { + if (!_options.experimental.replay.isEnabled) { return event; } - if (event.exceptions?.isNotEmpty == true && !hasFlushedReplay) { + // 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; + _hasFlushedReplay = true; } final sentryId = await _binding.getReplayId(); + if (sentryId == null) { + return event; + } event = event.copyWith(tags: { ...?event.tags, 'replayId': sentryId.toString(), }); } catch (exception, stackTrace) { - // todo: log + _options.logger( + SentryLevel.error, + 'Failed to apply $WebReplayEventProcessor', + exception: exception, + stackTrace: stackTrace, + ); } - return event; } } 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( From ffc8af6d750b7fa2143b0f4fdd2887850837ba49 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 26 Aug 2024 15:59:36 +0200 Subject: [PATCH 14/15] update --- flutter/example/ios/Runner/AppDelegate.swift | 2 +- flutter/lib/src/web/sentry_js_bridge.dart | 28 +++++++++----------- flutter/lib/src/web/sentry_web_interop.dart | 15 ++++++----- 3 files changed, 22 insertions(+), 23 deletions(-) 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/lib/src/web/sentry_js_bridge.dart b/flutter/lib/src/web/sentry_js_bridge.dart index d25e286a74..c6c559c350 100644 --- a/flutter/lib/src/web/sentry_js_bridge.dart +++ b/flutter/lib/src/web/sentry_js_bridge.dart @@ -15,50 +15,48 @@ abstract class SentryJsApi { class SentryJsWrapper implements SentryJsApi { @override - void init(JSAny? options) => SentryJsBridge.init(options); + void init(JSAny? options) => _SentryJsBridge.init(options); @override - void close() => SentryJsBridge.close(); + void close() => _SentryJsBridge.close(); @override - SentryJsClient getClient() => SentryJsBridge.getClient(); + SentryJsClient getClient() => _SentryJsBridge.getClient(); @override SentryJsReplay replayIntegration(JSAny? configuration) => - SentryJsBridge.replayIntegration(configuration); + _SentryJsBridge.replayIntegration(configuration); @override - JSAny? replayCanvasIntegration() => SentryJsBridge.replayCanvasIntegration(); + JSAny? replayCanvasIntegration() => _SentryJsBridge.replayCanvasIntegration(); @override JSAny? browserTracingIntegration() => - SentryJsBridge.browserTracingIntegration(); + _SentryJsBridge.browserTracingIntegration(); @override - SentryJsSession? getSession() => SentryJsBridge.getSession(); + SentryJsSession? getSession() => _SentryJsBridge.getSession(); @override - void captureSession() => SentryJsBridge.captureSession(); + void captureSession() => _SentryJsBridge.captureSession(); @override - JSAny? breadcrumbsIntegration() => SentryJsBridge.breadcrumbsIntegration(); + JSAny? breadcrumbsIntegration() => _SentryJsBridge.breadcrumbsIntegration(); } @internal @JS('Sentry') @staticInterop -class SentryJsBridge { +class _SentryJsBridge { external static void init(JSAny? options); - external static void close(); - - external static SentryJsReplay replayIntegration(JSAny? configuration); + external static JSAny? replayIntegration(JSAny? configuration); external static JSAny? replayCanvasIntegration(); - external static SentryJsClient getClient(); + external static void close(); - external static JSAny? getReplay(); + external static SentryJsClient getClient(); external static void captureSession(); diff --git a/flutter/lib/src/web/sentry_web_interop.dart b/flutter/lib/src/web/sentry_web_interop.dart index 890de0f4b5..10143e6d23 100644 --- a/flutter/lib/src/web/sentry_web_interop.dart +++ b/flutter/lib/src/web/sentry_web_interop.dart @@ -53,13 +53,14 @@ class SentryWebInterop 'replaysSessionSampleRate': options.experimental.replay.sessionSampleRate, 'replaysOnErrorSampleRate': options.experimental.replay.errorSampleRate, - // using defaultIntegrations ensures the we can control which integrations are added + // using defaultIntegrations ensures that we can control which integrations are added 'defaultIntegrations': [ _replay, _jsBridge.replayCanvasIntegration(), - // todo: check which default browser integrations make sense - // todo: test if the breadcrumbs make sense - _jsBridge.breadcrumbsIntegration(), + // 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() ], @@ -75,11 +76,11 @@ class SentryWebInterop @override Future captureEnvelope(SentryEnvelope envelope) async { return tryCatchAsync('captureEnvelope', () async { - final List jsItems = []; + final List envelopeItems = []; for (final item in envelope.items) { final originalObject = item.originalObject; - jsItems.add([ + envelopeItems.add([ (await item.header.toJson()), (await originalObject?.getPayload()) ]); @@ -97,7 +98,7 @@ class SentryWebInterop } } - final jsEnvelope = [envelope.header.toJson(), jsItems].jsify(); + final jsEnvelope = [envelope.header.toJson(), envelopeItems].jsify(); _jsBridge.getClient().sendEnvelope(jsEnvelope); }); From 5a9b212c9a55bc6df91edc3c5bf6346f33f484b5 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 26 Aug 2024 15:59:47 +0200 Subject: [PATCH 15/15] update --- .../lib/src/client_reports/client_report.dart | 6 +++++- dart/lib/src/metrics/metric.dart | 19 +++++++++++++++++++ dart/lib/src/protocol/sentry_event.dart | 10 +++++++++- .../sentry_attachment/sentry_attachment.dart | 8 +++++++- dart/lib/src/sentry_envelope.dart | 2 +- dart/lib/src/sentry_envelope_item.dart | 16 ++++++++++------ dart/lib/src/sentry_user_feedback.dart | 8 +++++++- 7 files changed, 58 insertions(+), 11 deletions(-) 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()); + } }