Skip to content

Commit

Permalink
feat: sentry web api (#2489)
Browse files Browse the repository at this point in the history
* update

* update

* update test

* update

* update

* update

* update

* update doc

* update

* update

* update

* update test

* update

* update tests

* use tryCatchSync

* formatting

* fix sentry_flutter_test

* fix analyze

* update test

* fix tests

* update test

* update test

* update scripts

* ignore sentry_web in coverage

* update test

* temporary

* update

* test

* update tests

* update test with mocks

* fix analyze

* update

* update names

* fix test

* update

* update integration

* update comment

* update flutter enricher

* update test

* update

* update flag name

* update

* update binding creation condition

* update

* update integration test

* fix tests
  • Loading branch information
buenaflor authored Jan 2, 2025
1 parent 7017668 commit 00bb02a
Show file tree
Hide file tree
Showing 45 changed files with 908 additions and 268 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/flutter_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,6 @@ jobs:
# Run the tests
flutter drive \
--driver=integration_test/test_driver/web_driver.dart \
--driver=integration_test/test_driver/driver.dart \
--target=integration_test/web_sdk_test.dart \
-d chrome
21 changes: 7 additions & 14 deletions dart/lib/src/platform_checker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,13 @@ class PlatformChecker {
}

/// Indicates whether a native integration is available.
bool get hasNativeIntegration {
if (isWeb) {
return false;
}
// We need to check the platform after we checked for web, because
// the OS checks return true when the browser runs on the checked platform.
// Example: platform.isAndroid return true if the browser is used on an
// Android device.
return platform.isAndroid ||
platform.isIOS ||
platform.isMacOS ||
platform.isWindows ||
platform.isLinux;
}
bool get hasNativeIntegration =>
isWeb ||
platform.isAndroid ||
platform.isIOS ||
platform.isMacOS ||
platform.isWindows ||
platform.isLinux;

static bool _isWebWithWasmSupport() {
if (const bool.hasEnvironment(_jsUtil)) {
Expand Down
3 changes: 0 additions & 3 deletions flutter/example/integration_test/test_driver/web_driver.dart

This file was deleted.

2 changes: 2 additions & 0 deletions flutter/example/integration_test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ FutureOr<void> restoreFlutterOnErrorAfter(FutureOr<void> Function() fn) async {
originalOnError?.call(details);
};
}

const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567';
76 changes: 40 additions & 36 deletions flutter/example/integration_test/web_sdk_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
@TestOn('browser')
library flutter_test;

import 'dart:async';
import 'dart:js';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
Expand All @@ -13,52 +13,56 @@ import 'package:sentry_flutter_example/main.dart' as app;

import 'utils.dart';

// We can use dart:html, this is meant to be tested on Flutter Web and not WASM
// This integration test can be changed later when we actually do support WASM
@JS('globalThis')
external JSObject get globalThis;

@JS('Sentry.getClient')
external JSObject? _getClient();

void main() {
group('Web SDK Integration', () {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

tearDown(() async {
await Sentry.close();
});

testWidgets('Sentry JS SDK is callable', (tester) async {
final completer = Completer();
const expectedMessage = 'test message';
String actualMessage = '';

await restoreFlutterOnErrorAfter(() async {
await SentryFlutter.init((options) {
options.dsn = app.exampleDsn;
options.automatedTestMode = false;
}, appRunner: () async {
await tester.pumpWidget(const app.MyApp());
group('enabled', () {
testWidgets('Sentry JS SDK initialized', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await SentryFlutter.init((options) {
options.enableSentryJs = true;
options.dsn = fakeDsn;
}, appRunner: () async {
await tester.pumpWidget(const app.MyApp());
});
});

final beforeSendFn = JsFunction.withThis((thisArg, event, hint) {
actualMessage = event['message'];
completer.complete();
return event;
});
expect(globalThis['Sentry'], isNotNull);

final Map<String, dynamic> options = {
'dsn': app.exampleDsn,
'beforeSend': beforeSendFn,
'defaultIntegrations': [],
};
final client = _getClient()!;
final options = client.callMethod('getOptions'.toJS)! as JSObject;

final sentry = context['Sentry'] as JsObject;
sentry.callMethod('init', [JsObject.jsify(options)]);
sentry.callMethod('captureMessage', [expectedMessage]);
});
final dsn = options.getProperty('dsn'.toJS).toString();
final defaultIntegrations = options
.getProperty('defaultIntegrations'.toJS)
.dartify() as List<Object?>;

await completer.future.timeout(const Duration(seconds: 5), onTimeout: () {
fail('beforeSend was not triggered');
expect(dsn, fakeDsn);
expect(defaultIntegrations, isEmpty);
});
});

expect(actualMessage, equals(expectedMessage));
group('disabled', () {
testWidgets('Sentry JS SDK is not initialized', (tester) async {
await restoreFlutterOnErrorAfter(() async {
await SentryFlutter.init((options) {
options.enableSentryJs = false;
options.dsn = fakeDsn;
}, appRunner: () async {
await tester.pumpWidget(const app.MyApp());
});
});

expect(globalThis['Sentry'], isNull);
expect(() => _getClient(), throwsA(anything));
});
});
});
}
1 change: 1 addition & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Future<void> setupSentry(
options.spotlight = Spotlight(enabled: true);
options.enableTimeToFullDisplayTracing = true;
options.enableMetrics = true;
options.enableSentryJs = true;

options.maxRequestBodySize = MaxRequestBodySize.always;
options.maxResponseBodySize = MaxResponseBodySize.always;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ class FlutterEnricherEventProcessor implements EventProcessor {
) async {
// If there's a native integration available, it probably has better
// information available than Flutter.
final device =
_hasNativeIntegration ? null : _getDevice(event.contexts.device);
// TODO: while we have a native integration with JS SDK, it's currently opt in and we dont gather contexts yet
// so for web it's still better to rely on the information of Flutter.
final device = _hasNativeIntegration && !_checker.isWeb
? null
: _getDevice(event.contexts.device);

final contexts = event.contexts.copyWith(
device: device,
Expand Down
2 changes: 1 addition & 1 deletion flutter/lib/src/integrations/integrations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export 'load_contexts_integration.dart';
export 'load_image_list_integration.dart';
export 'load_release_integration.dart';
export 'native_app_start_integration.dart';
export 'native_sdk_integration.dart';
export 'on_error_integration.dart';
export 'sdk_integration.dart';
export 'widgets_binding_integration.dart';
export 'widgets_flutter_binding_integration.dart';
6 changes: 6 additions & 0 deletions flutter/lib/src/integrations/native_sdk_integration.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import 'dart:async';

import 'package:sentry/sentry.dart';

import '../native/sentry_native_binding.dart';
import '../sentry_flutter_options.dart';

Integration<SentryFlutterOptions> createSdkIntegration(
SentryNativeBinding native) {
return NativeSdkIntegration(native);
}

/// Enables Sentry's native SDKs (Android and iOS) with options.
class NativeSdkIntegration implements Integration<SentryFlutterOptions> {
NativeSdkIntegration(this._native);
Expand Down
3 changes: 3 additions & 0 deletions flutter/lib/src/integrations/sdk_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'native_sdk_integration.dart'
if (dart.library.html) 'web_sdk_integration.dart'
if (dart.library.js_interop) 'web_sdk_integration.dart';
37 changes: 31 additions & 6 deletions flutter/lib/src/integrations/web_sdk_integration.dart
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
import 'dart:async';

import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';

import '../../sentry_flutter.dart';
import '../native/sentry_native_binding.dart';
import '../sentry_flutter_options.dart';
import '../web/script_loader/sentry_script_loader.dart';
import '../web/sentry_js_bundle.dart';

Integration<SentryFlutterOptions> createSdkIntegration(
SentryNativeBinding native) {
final scriptLoader = SentryScriptLoader();
return WebSdkIntegration(native, scriptLoader);
}

class WebSdkIntegration implements Integration<SentryFlutterOptions> {
WebSdkIntegration(this._scriptLoader);
WebSdkIntegration(this._web, this._scriptLoader);

final SentryNativeBinding _web;
final SentryScriptLoader _scriptLoader;
SentryFlutterOptions? _options;

@internal
static const name = 'webSdkIntegration';

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) async {
if (!options.enableSentryJs || !options.autoInitializeNativeSdk) {
return;
}

_options = options;

try {
final scripts = options.platformChecker.isDebugMode()
? debugScripts
: productionScripts;
await _scriptLoader.loadWebSdk(scripts);

await _web.init(hub);
options.sdk.addIntegration(name);
} catch (exception, stackTrace) {
options.logger(
SentryLevel.fatal,
'$name failed to be installed',
'$name failed to be installed.',
exception: exception,
stackTrace: stackTrace,
);
Expand All @@ -37,7 +53,16 @@ class WebSdkIntegration implements Integration<SentryFlutterOptions> {
}

@override
FutureOr<void> close() {
// no-op
FutureOr<void> close() async {
try {
await _web.close();
await _scriptLoader.close();
} catch (error, stackTrace) {
_options?.logger(SentryLevel.warning, '$name failed to be closed.',
exception: error, stackTrace: stackTrace);
if (_options?.automatedTestMode == true) {
rethrow;
}
}
}
}
2 changes: 1 addition & 1 deletion flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding {
@override
FutureOr<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException) {
throw UnsupportedError('$SentryNative.captureEnvelope() is not suppurted');
throw UnsupportedError('$SentryNative.captureEnvelope() is not supported');
}

@override
Expand Down
6 changes: 4 additions & 2 deletions flutter/lib/src/native/factory_web.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import '../../sentry_flutter.dart';
import '../web/sentry_js_binding.dart';
import '../web/sentry_web.dart';
import 'sentry_native_binding.dart';

// This isn't actually called, see SentryFlutter.init()
SentryNativeBinding createBinding(SentryFlutterOptions options) {
throw UnsupportedError("Native binding is not supported on this platform.");
final binding = createJsBinding();
return SentryWeb(binding, options);
}
2 changes: 1 addition & 1 deletion flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../replay/replay_config.dart';
import 'method_channel_helper.dart';
import 'native_app_start.dart';
import 'native_frames.dart';
import 'method_channel_helper.dart';
import 'sentry_native_binding.dart';
import 'sentry_native_invoker.dart';
import 'sentry_safe_method_channel.dart';
Expand Down
11 changes: 5 additions & 6 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
// ignore: implementation_imports
import 'package:sentry/src/sentry_tracer.dart';

import '../../sentry_flutter.dart';
import '../event_processor/flutter_enricher_event_processor.dart';
import '../native/native_frames.dart';
import '../native/sentry_native_binding.dart';
import 'time_to_display_tracker.dart';
import 'time_to_full_display_tracker.dart';

import '../../sentry_flutter.dart';
import '../event_processor/flutter_enricher_event_processor.dart';

// ignore: implementation_imports
import 'package:sentry/src/sentry_tracer.dart';

/// This key must be used so that the web interface displays the events nicely
/// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/
const _navigationKey = 'navigation';
Expand Down
35 changes: 17 additions & 18 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import 'integrations/frames_tracking_integration.dart';
import 'integrations/integrations.dart';
import 'integrations/native_app_start_handler.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';
Expand All @@ -29,7 +28,6 @@ import 'renderer/renderer.dart';
import 'replay/integration.dart';
import 'version.dart';
import 'view_hierarchy/view_hierarchy_integration.dart';
import 'web/script_loader/sentry_script_loader.dart';

/// Configuration options callback
typedef FlutterOptionsConfiguration = FutureOr<void> Function(
Expand Down Expand Up @@ -127,7 +125,9 @@ mixin SentryFlutter {
if (_native!.supportsCaptureEnvelope) {
options.transport = FileSystemTransport(_native!, options);
}
options.addScopeObserver(NativeScopeObserver(_native!));
if (!options.platformChecker.isWeb) {
options.addScopeObserver(NativeScopeObserver(_native!));
}
}

options.addEventProcessor(FlutterEnricherEventProcessor(options));
Expand Down Expand Up @@ -170,22 +170,23 @@ mixin SentryFlutter {

// The ordering here matters, as we'd like to first start the native integration.
// That allow us to send events to the network and then the Flutter integrations.
// Flutter Web doesn't need that, only Android and iOS.
final native = _native;
if (native != null) {
integrations.add(NativeSdkIntegration(native));
if (native.supportsLoadContexts) {
integrations.add(LoadContextsIntegration(native));
integrations.add(createSdkIntegration(native));
if (!platformChecker.isWeb) {
if (native.supportsLoadContexts) {
integrations.add(LoadContextsIntegration(native));
}
integrations.add(LoadImageListIntegration(native));
integrations.add(FramesTrackingIntegration(native));
integrations.add(
NativeAppStartIntegration(
DefaultFrameCallbackHandler(),
NativeAppStartHandler(native),
),
);
integrations.add(ReplayIntegration(native));
}
integrations.add(LoadImageListIntegration(native));
integrations.add(FramesTrackingIntegration(native));
integrations.add(
NativeAppStartIntegration(
DefaultFrameCallbackHandler(),
NativeAppStartHandler(native),
),
);
integrations.add(ReplayIntegration(native));
options.enableDartSymbolication = false;
}

Expand All @@ -195,8 +196,6 @@ mixin SentryFlutter {
}

if (platformChecker.isWeb) {
final loader = SentryScriptLoader(options);
integrations.add(WebSdkIntegration(loader));
integrations.add(ConnectivityIntegration());
}

Expand Down
Loading

0 comments on commit 00bb02a

Please sign in to comment.