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

feat(js-sdk): add script loader to set up Sentry Javascript SDK #2406

Merged
merged 77 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 71 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
4524383
update
buenaflor Nov 12, 2024
02bde65
update
buenaflor Nov 12, 2024
3a99531
updaet
buenaflor Nov 12, 2024
bb94847
Update sentry_js_sdk_version.dart
buenaflor Nov 12, 2024
c6c1cbd
temporary ci change
buenaflor Nov 12, 2024
b837667
fix test
buenaflor Nov 12, 2024
e535701
fix compilation
buenaflor Nov 12, 2024
0f8c6fc
fix compilation
buenaflor Nov 12, 2024
01c2738
fix
buenaflor Nov 12, 2024
e5179e4
fix test
buenaflor Nov 12, 2024
59b4ab8
update
buenaflor Nov 13, 2024
620071e
Merge branch 'main' into feat/sentry-script-loader
buenaflor Nov 20, 2024
c3328a4
fix test
buenaflor Nov 20, 2024
f64f749
fix test
buenaflor Nov 20, 2024
9aba0c4
update
buenaflor Nov 20, 2024
818f8a4
update
buenaflor Nov 20, 2024
15bb56c
update
buenaflor Nov 20, 2024
e2b2251
fix analyze
buenaflor Nov 20, 2024
adad5d9
update
buenaflor Nov 20, 2024
e01a549
update
buenaflor Nov 20, 2024
818d56e
update validation
buenaflor Nov 20, 2024
bd3c46e
update
buenaflor Nov 20, 2024
797097b
update
buenaflor Nov 20, 2024
009c8ea
update
buenaflor Nov 20, 2024
3b3f78c
rethrow on automated test mode
buenaflor Nov 20, 2024
8be2059
update rethrow
buenaflor Nov 20, 2024
8de74d3
update
buenaflor Nov 25, 2024
ce02246
update tests
buenaflor Nov 25, 2024
fca9add
update appending
buenaflor Nov 27, 2024
b3009c8
update web
buenaflor Nov 27, 2024
15d16db
add trusted types and add tests
buenaflor Dec 2, 2024
a7b6054
formatting
buenaflor Dec 2, 2024
2ef43fd
update comment
buenaflor Dec 2, 2024
21b1382
add as browser test
buenaflor Dec 2, 2024
af1f7cb
fix min_version
buenaflor Dec 2, 2024
dbfe736
fix warnings
buenaflor Dec 2, 2024
8538c9c
add another test file
buenaflor Dec 2, 2024
90aaedc
update
buenaflor Dec 2, 2024
c57b950
update comment
buenaflor Dec 2, 2024
42aa1f8
try out web integration test
buenaflor Dec 2, 2024
77dd5d8
update
buenaflor Dec 2, 2024
f9c4532
use ubuntu latest for web
buenaflor Dec 2, 2024
fab4044
see if it runs
buenaflor Dec 3, 2024
b128ede
update
buenaflor Dec 3, 2024
3e3a87e
fix job
buenaflor Dec 3, 2024
de34883
fix job
buenaflor Dec 3, 2024
cb21548
run chromedriver in background
buenaflor Dec 3, 2024
fdd6353
run correct port
buenaflor Dec 3, 2024
7115c4b
update
buenaflor Dec 3, 2024
621da63
update
buenaflor Dec 3, 2024
5f4db66
setup chrome action
buenaflor Dec 3, 2024
927dc65
update
buenaflor Dec 3, 2024
a8f4b45
run chrome first
buenaflor Dec 3, 2024
d7daa56
try
buenaflor Dec 3, 2024
57cf95f
update
buenaflor Dec 3, 2024
09264b1
update
buenaflor Dec 3, 2024
e79977f
remove file
buenaflor Dec 3, 2024
a433e9f
update
buenaflor Dec 3, 2024
4987b8a
update
buenaflor Dec 3, 2024
5c237a2
fix tests
buenaflor Dec 3, 2024
1c9010a
update
buenaflor Dec 3, 2024
cac20f3
update docs and test cases
buenaflor Dec 4, 2024
d441a0b
add todo
buenaflor Dec 4, 2024
fb2494f
update
buenaflor Dec 4, 2024
5c4c498
update
buenaflor Dec 4, 2024
f78adc0
add doc and fix tests
buenaflor Dec 4, 2024
0584a74
update tests
buenaflor Dec 4, 2024
8597192
update
buenaflor Dec 5, 2024
6c57cc6
update
buenaflor Dec 5, 2024
485f196
Update web_sdk_test.dart
buenaflor Dec 5, 2024
bf38ab3
add test with automatedTestMode false
buenaflor Dec 5, 2024
cfd36a0
update
buenaflor Dec 6, 2024
627ac06
remove debug
buenaflor Dec 10, 2024
7f02fa2
remove fn
buenaflor Dec 10, 2024
1a75a61
update restore flutter onError
buenaflor Dec 10, 2024
8592e11
fix analyze
buenaflor Dec 10, 2024
3f97c2e
Update flutter.yml
buenaflor Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches:
- main
- release/**
- feat/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove before merging, this is so ci runs since we merge into another feat branch

pull_request:
paths:
- '.github/workflows/flutter.yml'
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/flutter_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,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/web_driver.dart \
--target=integration_test/web_sdk_test.dart \
-d chrome
3 changes: 3 additions & 0 deletions flutter/example/integration_test/test_driver/web_driver.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();
9 changes: 9 additions & 0 deletions flutter/example/integration_test/utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:sentry_flutter/sentry_flutter.dart';

const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567';

void defaultTestOptionsInitializer(SentryFlutterOptions options) {
options.dsn = fakeDsn;
// ignore: invalid_use_of_internal_member
options.automatedTestMode = true;
}
54 changes: 54 additions & 0 deletions flutter/example/integration_test/web_sdk_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// ignore_for_file: invalid_use_of_internal_member

@TestOn('browser')
library flutter_test;

// ignore: avoid_web_libraries_in_flutter
import 'dart:js';
import 'dart:js_interop';

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_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

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

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

testWidgets('Sentry JS SDK is callable', (tester) async {
await SentryFlutter.init(defaultTestOptionsInitializer,
appRunner: () async {
await tester.pumpWidget(const app.MyApp());
});

const expectedMessage = 'test message';
final beforeSendFn = JsFunction.withThis((thisArg, event, hint) {
final actualMessage = event['message'];
expect(actualMessage, equals(expectedMessage));

return event;
});

final Map<String, dynamic> options = {
'dsn': app.exampleDsn,
'beforeSend': beforeSendFn,
'defaultIntegrations': [],
};

final sentry = context['Sentry'] as JsObject;
sentry.callMethod('init', [JsObject.jsify(options)]);

sentry.callMethod('captureMessage', [expectedMessage.toJS]);
});
});
}
43 changes: 43 additions & 0 deletions flutter/lib/src/integrations/web_sdk_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:async';

import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../web/script_loader/sentry_script_loader.dart';
import '../web/sentry_js_bundle.dart';

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

final SentryScriptLoader _scriptLoader;

@internal
static const name = 'webSdkIntegration';

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) async {
try {
final scripts = options.platformChecker.isDebugMode()
? debugScripts
: productionScripts;
await _scriptLoader.loadWebSdk(scripts);

options.sdk.addIntegration(name);
} catch (exception, stackTrace) {
options.logger(

Check warning on line 27 in flutter/lib/src/integrations/web_sdk_integration.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/integrations/web_sdk_integration.dart#L27

Added line #L27 was not covered by tests
SentryLevel.fatal,
'$name failed to be installed',
exception: exception,
stackTrace: stackTrace,
);
if (options.automatedTestMode) {

Check warning on line 33 in flutter/lib/src/integrations/web_sdk_integration.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/integrations/web_sdk_integration.dart#L33

Added line #L33 was not covered by tests
rethrow;
}
}
}

@override
FutureOr<void> close() {
// no-op
}
}
4 changes: 4 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ 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';
import 'profiling.dart';
import 'renderer/renderer.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 @@ -180,6 +182,8 @@ mixin SentryFlutter {
}

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

Expand Down
59 changes: 59 additions & 0 deletions flutter/lib/src/web/script_loader/html_script_dom_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:async';
import 'dart:html';
import 'dart:js_util' as js_util;

import '../../../sentry_flutter.dart';
import 'sentry_script_loader.dart';

Future<void> loadScript(String src, SentryOptions options,
{String? integrity,
String trustedTypePolicyName = defaultTrustedPolicyName}) {
final completer = Completer<void>();

final script = ScriptElement()
..crossOrigin = 'anonymous'
..onLoad.listen((_) => completer.complete())
..onError.listen((event) => completer.completeError('Failed to load $src'));

TrustedScriptUrl? trustedUrl;

// If TrustedTypes are available, prepare a trusted URL
final trustedTypes = js_util.getProperty<dynamic>(window, 'trustedTypes');
if (trustedTypes != null) {
try {
final policy =
js_util.callMethod<dynamic>(trustedTypes as Object, 'createPolicy', [
trustedTypePolicyName,
js_util.jsify({
'createScriptURL': (String url) => src,
})
]);
trustedUrl =
js_util.callMethod(policy as Object, 'createScriptURL', [src]);
} catch (e) {
// will be caught by loadWebSdk
throw TrustedTypesException();
}
}

if (trustedUrl != null) {
js_util.setProperty(script, 'src', trustedUrl);
} else {
script.src = src;
}

if (integrity != null) {
script.integrity = integrity;
}

// JS SDK needs to be loaded before everything else
final head = document.head;
if (head != null) {
if (head.hasChildNodes()) {
head.insertBefore(script, head.firstChild);
} else {
head.append(script);
}
}
return completer.future;
}
6 changes: 6 additions & 0 deletions flutter/lib/src/web/script_loader/noop_script_dom_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '../../../sentry_flutter.dart';
import 'sentry_script_loader.dart';

Future<void> loadScript(String src, SentryOptions options,
{String? integrity,
String trustedTypePolicyName = defaultTrustedPolicyName}) async {}
3 changes: 3 additions & 0 deletions flutter/lib/src/web/script_loader/script_dom_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'noop_script_dom_api.dart'
if (dart.library.html) 'html_script_dom_api.dart'
if (dart.library.js_interop) 'web_script_dom_api.dart';
60 changes: 60 additions & 0 deletions flutter/lib/src/web/script_loader/sentry_script_loader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'dart:async';

import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import 'script_dom_api.dart';

@internal
const String defaultTrustedPolicyName = 'sentry-dart';

class SentryScriptLoader {
SentryScriptLoader(this._options);

final SentryFlutterOptions _options;
bool _scriptLoaded = false;

/// Loads the scripts into the web page with support for Trusted Types security policy.
///
/// The function handles three Trusted Types scenarios:
/// 1. No Trusted Types configured - Scripts load normally
/// 2. Custom Trusted Types policy - Uses provided policy name to create trusted URLs
/// 3. Trusted Types forbidden - Scripts are not loaded
///
/// The function is only executed successfully once and will be guarded by a flag afterwards.
///
/// TrustedTypes implementation inspired by https://pub.dev/packages/google_identity_services_web
Future<void> loadWebSdk(List<Map<String, String>> scripts,
{String trustedTypePolicyName = defaultTrustedPolicyName}) async {
if (_scriptLoaded) return;

try {
await Future.forEach(scripts, (Map<String, String> script) async {
final url = script['url'];
final integrity = script['integrity'];

if (url != null) {
await loadScript(url, _options,
integrity: integrity,
trustedTypePolicyName: trustedTypePolicyName);
}
});

_scriptLoaded = true;
_options.logger(SentryLevel.debug,
'JS SDK integration: all Sentry scripts loaded successfully.');
} catch (e) {
_options.logger(SentryLevel.error, 'Failed to load Sentry scripts: $e');
if (_options.automatedTestMode) {

Check warning on line 48 in flutter/lib/src/web/script_loader/sentry_script_loader.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/web/script_loader/sentry_script_loader.dart#L47-L48

Added lines #L47 - L48 were not covered by tests
rethrow;
}
}
}
}

/// Exception thrown if the Trusted Types feature is supported, enabled, and it
/// has prevented this loader from injecting the Sentry JS SDK
@internal
class TrustedTypesException implements Exception {
TrustedTypesException();

Check warning on line 59 in flutter/lib/src/web/script_loader/sentry_script_loader.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/web/script_loader/sentry_script_loader.dart#L59

Added line #L59 was not covered by tests
}
Loading
Loading