Skip to content

Commit

Permalink
v0 js sdk integration (#2572)
Browse files Browse the repository at this point in the history
* feat(js-sdk): add script loader to set up Sentry Javascript SDK (#2406)

* update

* update

* updaet

* Update sentry_js_sdk_version.dart

* temporary ci change

* fix test

* fix compilation

* fix compilation

* fix

* fix test

* update

* fix test

* fix test

* update

* update

* update

* fix analyze

* update

* update

* update validation

* update

* update

* update

* rethrow on automated test mode

* update rethrow

* update

* update tests

* update appending

* update web

* add trusted types and add tests

* formatting

* update comment

* add as browser test

* fix min_version

* fix warnings

* add another test file

* update

* update comment

* try out web integration test

* update

* use ubuntu latest for web

* see if it runs

* update

* fix job

* fix job

* run chromedriver in background

* run correct port

* update

* update

* setup chrome action

* update

* run chrome first

* try

* update

* update

* remove file

* update

* update

* fix tests

* update

* update docs and test cases

* add todo

* update

* update

* add doc and fix tests

* update tests

* update

* update

* Update web_sdk_test.dart

* add test with automatedTestMode false

* update

* remove debug

* remove fn

* update restore flutter onError

* fix analyze

* Update flutter.yml

* feat: sentry web api (#2489)

* 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

* feat(js-sdk): enable global error handler and dedupe integration (#2566)

* add default integrations

* fix integration test

* feat(js-sdk): use js transport (#2563)

* add envelope sending

* add test

* add test

* update message

* update payload api

* update

* update payload api

* revert hasNativeIntegration

* update

* dont remove scope observer

* fix integration test

* cleanup

* update test

* update test

* cleanup integration test

* improve envelope capturing

* clean up

* fix analyze

* fix tests

* update

* formatting

* fix integration test

* use buckets

* fix log string

* update mocks

* update mocks

* process succeessful envelope items

* update CHANGELOG
  • Loading branch information
buenaflor authored Jan 14, 2025
1 parent ec50b21 commit f27eaee
Show file tree
Hide file tree
Showing 50 changed files with 2,005 additions and 120 deletions.
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 # [email protected]
with:
chrome-version: stable
- run: chrome --version

- uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # [email protected]
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 # [email protected]

- 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 @@ 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
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://[email protected]/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

0 comments on commit f27eaee

Please sign in to comment.