Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0 js sdk integration #2572

Merged
merged 12 commits into from
Jan 14, 2025
Merged
59 changes: 59 additions & 0 deletions .github/workflows/flutter_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,62 @@ jobs:
if: ${{ matrix.target != 'macos' }}
working-directory: ./flutter/example/${{ matrix.target }}
run: xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug -destination "platform=${{ steps.device.outputs.platform }}" -allowProvisioningUpdates CODE_SIGNING_ALLOWED=NO

test-web:
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: ./flutter/example
strategy:
fail-fast: false
matrix:
sdk: [ "stable", "beta" ]
steps:
- name: checkout
uses: actions/checkout@v4

- name: Install Chrome Browser
uses: browser-actions/setup-chrome@facf10a55b9caf92e0cc749b4f82bf8220989148 # pin@v1.7.2
with:
chrome-version: stable
- run: chrome --version

- uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # pin@v2.16.0
with:
channel: ${{ matrix.sdk }}

- name: flutter upgrade
run: flutter upgrade

- name: flutter pub get
run: flutter pub get

- name: Install Xvfb and dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb
sudo apt-get -y install xorg xvfb gtk2-engines-pixbuf
sudo apt-get -y install dbus-x11 xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable
sudo apt-get -y install imagemagick x11-apps

- name: Setup ChromeDriver
uses: nanasess/setup-chromedriver@e93e57b843c0c92788f22483f1a31af8ee48db25 # pin@2.3.0

- name: Start Xvfb and run tests
run: |
# Start Xvfb with specific screen settings
Xvfb -ac :99 -screen 0 1280x1024x16 &
export DISPLAY=:99

# Start ChromeDriver
chromedriver --port=4444 &

# Wait for services to start
sleep 5

# Run the tests
flutter drive \
--driver=integration_test/test_driver/driver.dart \
--target=integration_test/web_sdk_test.dart \
-d chrome
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

- Add `beforeCapture` for View Hierarchy ([#2523](https://github.com/getsentry/sentry-dart/pull/2523))
- View hierarchy calls are now debounced for 2 seconds.
- JS SDK integration ([#2572](https://github.com/getsentry/sentry-dart/pull/2572))
- Enable the integration by setting `options.enableSentryJs = true`
- Features:
- Sending envelopes through Sentry JS transport layer
- Capturing native JS errors

### Enhancements

Expand Down
2 changes: 1 addition & 1 deletion dart/lib/src/client_reports/client_report.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:meta/meta.dart';

import 'discarded_event.dart';
import '../utils.dart';
import 'discarded_event.dart';

@internal
class ClientReport {
Expand Down
1 change: 1 addition & 0 deletions dart/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

22 changes: 8 additions & 14 deletions dart/lib/src/platform_checker.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';

import 'platform/platform.dart';

/// Helper to check in which environment the library is running.
Expand Down Expand Up @@ -40,20 +41,13 @@
}

/// 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;

Check warning on line 50 in dart/lib/src/platform_checker.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/platform_checker.dart#L44-L50

Added lines #L44 - L50 were not covered by tests

static bool _isWebWithWasmSupport() {
if (const bool.hasEnvironment(_jsUtil)) {
Expand Down
2 changes: 1 addition & 1 deletion dart/lib/src/protocol/sentry_event.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';

import '../protocol.dart';
import '../throwable_mechanism.dart';
Expand Down
12 changes: 6 additions & 6 deletions dart/lib/src/transport/http_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart';
import '../utils/transport_utils.dart';
import 'http_transport_request_handler.dart';

import '../http_client/client_provider.dart'
if (dart.library.io) '../http_client/io_client_provider.dart';
import '../noop_client.dart';
import '../protocol.dart';
import '../sentry_options.dart';
import '../sentry_envelope.dart';
import 'transport.dart';
import '../sentry_options.dart';
import '../utils/transport_utils.dart';
import 'http_transport_request_handler.dart';
import 'rate_limiter.dart';
import '../http_client/client_provider.dart'
if (dart.library.io) '../http_client/io_client_provider.dart';
import 'transport.dart';

/// A transport is in charge of sending the event to the Sentry server.
class HttpTransport implements Transport {
Expand Down
6 changes: 4 additions & 2 deletions flutter/example/integration_test/utils.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';

/// Restores Flutter's `FlutterError.onError` to its original state after executing a function.
///
Expand All @@ -9,7 +9,7 @@ import 'package:flutter/cupertino.dart';
/// Flutter will complain and throw an error.
///
/// This function ensures `FlutterError.onError` is restored to its initial state after `fn` runs.
/// It must be called **after** the function executes but **before** any assertions.
/// Assertions must only be executed after onError has been restored.
FutureOr<void> restoreFlutterOnErrorAfter(FutureOr<void> Function() fn) async {
final originalOnError = FlutterError.onError;
await fn();
Expand All @@ -20,3 +20,5 @@ FutureOr<void> restoreFlutterOnErrorAfter(FutureOr<void> Function() fn) async {
originalOnError?.call(details);
};
}

const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567';
145 changes: 145 additions & 0 deletions flutter/example/integration_test/web_sdk_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// ignore_for_file: invalid_use_of_internal_member, avoid_web_libraries_in_flutter

@TestOn('browser')
library flutter_test;

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

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/web/javascript_transport.dart';
import 'package:sentry_flutter_example/main.dart' as app;

import 'utils.dart';

@JS('globalThis')
external JSObject get globalThis;

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

@JS()
@staticInterop
class SentryClient {
external factory SentryClient();
}

extension _SentryClientExtension on SentryClient {
external void on(JSString event, JSFunction callback);

external SentryOptions getOptions();
}

@JS()
@staticInterop
class SentryOptions {
external factory SentryOptions();
}

extension _SentryOptionsExtension on SentryOptions {
external JSString get dsn;
}

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

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());
});
});

expect(globalThis['Sentry'], isNotNull);

final client = _getClient()!;
final options = client.getOptions();

final dsn = options.dsn.toDart;

await Sentry.captureException(Exception('test'));

expect(dsn, fakeDsn);
});

testWidgets('sends the correct envelope', (tester) async {
SentryFlutterOptions? configuredOptions;
SentryEvent? dartEvent;

await restoreFlutterOnErrorAfter(() async {
await SentryFlutter.init((options) {
options.enableSentryJs = true;
options.dsn = fakeDsn;
options.beforeSend = (event, hint) {
dartEvent = event;
return event;
};
configuredOptions = options;
}, appRunner: () async {
await tester.pumpWidget(const app.MyApp());
});
});

expect(configuredOptions!.transport, isA<JavascriptTransport>());

final client = _getClient()!;
final completer = Completer<List<Object?>>();

JSFunction beforeEnvelopeCallback = ((JSArray envelope) {
final envelopDart = envelope.dartify() as List<Object?>;
completer.complete(envelopDart);
}).toJS;

client.on('beforeEnvelope'.toJS, beforeEnvelopeCallback);

final id = await Sentry.captureException(Exception('test'));

final envelope = await completer.future;

final header = envelope.first as Map<Object?, Object?>;
expect(header['event_id'], id.toString());
expect((header['sdk'] as Map<Object?, Object?>)['name'],
'sentry.dart.flutter');

final item = (envelope[1] as List<Object?>).first as List<Object?>;
final itemPayload =
json.decode(utf8.decoder.convert((item[1] as List<int>)))
as Map<Object?, Object?>;

final jsEventJson = (itemPayload).map((key, value) {
return MapEntry(key.toString(), value as dynamic);
});
final dartEventJson = dartEvent!.toJson();

// Make sure what we send from the Flutter layer is the same as what's being
// sent in the JS layer
expect(jsEventJson, equals(dartEventJson));
});
});

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';
Loading
Loading