Skip to content

Commit 2e89903

Browse files
authored
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
1 parent 13c8257 commit 2e89903

24 files changed

+845
-0
lines changed

.github/workflows/flutter_test.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,62 @@ jobs:
166166
if: ${{ matrix.target != 'macos' }}
167167
working-directory: ./flutter/example/${{ matrix.target }}
168168
run: xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug -destination "platform=${{ steps.device.outputs.platform }}" -allowProvisioningUpdates CODE_SIGNING_ALLOWED=NO
169+
170+
test-web:
171+
runs-on: ubuntu-latest
172+
timeout-minutes: 30
173+
defaults:
174+
run:
175+
working-directory: ./flutter/example
176+
strategy:
177+
fail-fast: false
178+
matrix:
179+
sdk: [ "stable", "beta" ]
180+
steps:
181+
- name: checkout
182+
uses: actions/checkout@v4
183+
184+
- name: Install Chrome Browser
185+
uses: browser-actions/setup-chrome@facf10a55b9caf92e0cc749b4f82bf8220989148 # [email protected]
186+
with:
187+
chrome-version: stable
188+
- run: chrome --version
189+
190+
- uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # [email protected]
191+
with:
192+
channel: ${{ matrix.sdk }}
193+
194+
- name: flutter upgrade
195+
run: flutter upgrade
196+
197+
- name: flutter pub get
198+
run: flutter pub get
199+
200+
- name: Install Xvfb and dependencies
201+
run: |
202+
sudo apt-get update
203+
sudo apt-get install -y xvfb
204+
sudo apt-get -y install xorg xvfb gtk2-engines-pixbuf
205+
sudo apt-get -y install dbus-x11 xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable
206+
sudo apt-get -y install imagemagick x11-apps
207+
208+
- name: Setup ChromeDriver
209+
uses: nanasess/setup-chromedriver@e93e57b843c0c92788f22483f1a31af8ee48db25 # [email protected]
210+
211+
- name: Start Xvfb and run tests
212+
run: |
213+
# Start Xvfb with specific screen settings
214+
Xvfb -ac :99 -screen 0 1280x1024x16 &
215+
export DISPLAY=:99
216+
217+
# Start ChromeDriver
218+
chromedriver --port=4444 &
219+
220+
# Wait for services to start
221+
sleep 5
222+
223+
# Run the tests
224+
flutter drive \
225+
--driver=integration_test/test_driver/web_driver.dart \
226+
--target=integration_test/web_sdk_test.dart \
227+
-d chrome

flutter/example/devtools_options.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
description: This file stores settings for Dart & Flutter DevTools.
2+
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3+
extensions:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import 'package:integration_test/integration_test_driver.dart';
2+
3+
Future<void> main() => integrationDriver();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'package:flutter/cupertino.dart';
2+
3+
/// Restores the onError to it's original state.
4+
/// This makes assertion errors readable.
5+
///
6+
/// testWidgets override Flutter.onError by default
7+
/// If a fail happens during integration tests this would complain that
8+
/// the FlutterError.onError was overwritten and wasn't reset to its
9+
/// state before asserting.
10+
///
11+
/// This function needs to be executed before assertions.
12+
Future<void> restoreFlutterOnErrorAfter(Future<void> Function() fn) async {
13+
final originalOnError = FlutterError.onError;
14+
await fn();
15+
final overriddenOnError = FlutterError.onError;
16+
17+
FlutterError.onError = (FlutterErrorDetails details) {
18+
if (overriddenOnError != originalOnError) overriddenOnError?.call(details);
19+
originalOnError?.call(details);
20+
};
21+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// ignore_for_file: invalid_use_of_internal_member, avoid_web_libraries_in_flutter
2+
3+
@TestOn('browser')
4+
library flutter_test;
5+
6+
import 'dart:async';
7+
import 'dart:js';
8+
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:integration_test/integration_test.dart';
11+
import 'package:sentry_flutter/sentry_flutter.dart';
12+
import 'package:sentry_flutter_example/main.dart' as app;
13+
14+
import 'utils.dart';
15+
16+
// We can use dart:html, this is meant to be tested on Flutter Web and not WASM
17+
// This integration test can be changed later when we actually do support WASM
18+
19+
void main() {
20+
group('Web SDK Integration', () {
21+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
22+
23+
tearDown(() async {
24+
await Sentry.close();
25+
});
26+
27+
testWidgets('Sentry JS SDK is callable', (tester) async {
28+
final completer = Completer();
29+
const expectedMessage = 'test message';
30+
String actualMessage = '';
31+
32+
await restoreFlutterOnErrorAfter(() async {
33+
await SentryFlutter.init((options) {
34+
options.dsn = app.exampleDsn;
35+
options.automatedTestMode = false;
36+
}, appRunner: () async {
37+
await tester.pumpWidget(const app.MyApp());
38+
});
39+
40+
final beforeSendFn = JsFunction.withThis((thisArg, event, hint) {
41+
actualMessage = event['message'];
42+
completer.complete();
43+
return event;
44+
});
45+
46+
final Map<String, dynamic> options = {
47+
'dsn': app.exampleDsn,
48+
'beforeSend': beforeSendFn,
49+
'defaultIntegrations': [],
50+
};
51+
52+
final sentry = context['Sentry'] as JsObject;
53+
sentry.callMethod('init', [JsObject.jsify(options)]);
54+
sentry.callMethod('captureMessage', [expectedMessage]);
55+
});
56+
57+
await completer.future.timeout(const Duration(seconds: 5), onTimeout: () {
58+
fail('beforeSend was not triggered');
59+
});
60+
61+
expect(actualMessage, equals(expectedMessage));
62+
});
63+
});
64+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'dart:async';
2+
3+
import 'package:meta/meta.dart';
4+
5+
import '../../sentry_flutter.dart';
6+
import '../web/script_loader/sentry_script_loader.dart';
7+
import '../web/sentry_js_bundle.dart';
8+
9+
class WebSdkIntegration implements Integration<SentryFlutterOptions> {
10+
WebSdkIntegration(this._scriptLoader);
11+
12+
final SentryScriptLoader _scriptLoader;
13+
14+
@internal
15+
static const name = 'webSdkIntegration';
16+
17+
@override
18+
FutureOr<void> call(Hub hub, SentryFlutterOptions options) async {
19+
try {
20+
final scripts = options.platformChecker.isDebugMode()
21+
? debugScripts
22+
: productionScripts;
23+
await _scriptLoader.loadWebSdk(scripts);
24+
25+
options.sdk.addIntegration(name);
26+
} catch (exception, stackTrace) {
27+
options.logger(
28+
SentryLevel.fatal,
29+
'$name failed to be installed',
30+
exception: exception,
31+
stackTrace: stackTrace,
32+
);
33+
if (options.automatedTestMode) {
34+
rethrow;
35+
}
36+
}
37+
}
38+
39+
@override
40+
FutureOr<void> close() {
41+
// no-op
42+
}
43+
}

flutter/lib/src/sentry_flutter.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import 'integrations/frames_tracking_integration.dart';
2020
import 'integrations/integrations.dart';
2121
import 'integrations/native_app_start_handler.dart';
2222
import 'integrations/screenshot_integration.dart';
23+
import 'integrations/web_sdk_integration.dart';
2324
import 'native/factory.dart';
2425
import 'native/native_scope_observer.dart';
2526
import 'native/sentry_native_binding.dart';
2627
import 'profiling.dart';
2728
import 'renderer/renderer.dart';
2829
import 'version.dart';
2930
import 'view_hierarchy/view_hierarchy_integration.dart';
31+
import 'web/script_loader/sentry_script_loader.dart';
3032

3133
/// Configuration options callback
3234
typedef FlutterOptionsConfiguration = FutureOr<void> Function(
@@ -180,6 +182,8 @@ mixin SentryFlutter {
180182
}
181183

182184
if (platformChecker.isWeb) {
185+
final loader = SentryScriptLoader(options);
186+
integrations.add(WebSdkIntegration(loader));
183187
integrations.add(ConnectivityIntegration());
184188
}
185189

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'dart:async';
2+
import 'dart:html';
3+
import 'dart:js_util' as js_util;
4+
5+
import '../../../sentry_flutter.dart';
6+
import 'sentry_script_loader.dart';
7+
8+
Future<void> loadScript(String src, SentryOptions options,
9+
{String? integrity,
10+
String trustedTypePolicyName = defaultTrustedPolicyName}) {
11+
final completer = Completer<void>();
12+
13+
final script = ScriptElement()
14+
..crossOrigin = 'anonymous'
15+
..onLoad.listen((_) => completer.complete())
16+
..onError.listen((event) => completer.completeError('Failed to load $src'));
17+
18+
TrustedScriptUrl? trustedUrl;
19+
20+
// If TrustedTypes are available, prepare a trusted URL
21+
final trustedTypes = js_util.getProperty<dynamic>(window, 'trustedTypes');
22+
if (trustedTypes != null) {
23+
try {
24+
final policy =
25+
js_util.callMethod<dynamic>(trustedTypes as Object, 'createPolicy', [
26+
trustedTypePolicyName,
27+
js_util.jsify({
28+
'createScriptURL': (String url) => src,
29+
})
30+
]);
31+
trustedUrl =
32+
js_util.callMethod(policy as Object, 'createScriptURL', [src]);
33+
} catch (e) {
34+
// will be caught by loadWebSdk
35+
throw TrustedTypesException();
36+
}
37+
}
38+
39+
if (trustedUrl != null) {
40+
js_util.setProperty(script, 'src', trustedUrl);
41+
} else {
42+
script.src = src;
43+
}
44+
45+
if (integrity != null) {
46+
script.integrity = integrity;
47+
}
48+
49+
// JS SDK needs to be loaded before everything else
50+
final head = document.head;
51+
if (head != null) {
52+
if (head.hasChildNodes()) {
53+
head.insertBefore(script, head.firstChild);
54+
} else {
55+
head.append(script);
56+
}
57+
}
58+
return completer.future;
59+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import '../../../sentry_flutter.dart';
2+
import 'sentry_script_loader.dart';
3+
4+
Future<void> loadScript(String src, SentryOptions options,
5+
{String? integrity,
6+
String trustedTypePolicyName = defaultTrustedPolicyName}) async {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export 'noop_script_dom_api.dart'
2+
if (dart.library.html) 'html_script_dom_api.dart'
3+
if (dart.library.js_interop) 'web_script_dom_api.dart';

0 commit comments

Comments
 (0)