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): use js transport #2563

Draft
wants to merge 23 commits into
base: feat/js-sdk-integration
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
7 changes: 5 additions & 2 deletions dart/lib/src/client_reports/client_report.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:meta/meta.dart';

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

@internal
class ClientReport {
class ClientReport implements SentryEnvelopeItemPayload {
ClientReport(this.timestamp, this.discardedEvents);

final DateTime? timestamp;
Expand All @@ -27,4 +27,7 @@ class ClientReport {

return json;
}

@override
Future<dynamic> asPayload() => Future.value(toJson());
}
1 change: 1 addition & 0 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
13 changes: 8 additions & 5 deletions dart/lib/src/protocol/sentry_event.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';

import '../protocol.dart';
import '../throwable_mechanism.dart';
import '../utils.dart';
import '../../sentry.dart';
import 'access_aware_map.dart';

/// An event to be reported to Sentry.io.
@immutable
class SentryEvent with SentryEventLike<SentryEvent> {
class SentryEvent
with SentryEventLike<SentryEvent>
implements SentryEnvelopeItemPayload {
/// Creates an event.
SentryEvent({
SentryId? eventId,
Expand Down Expand Up @@ -418,4 +418,7 @@ class SentryEvent with SentryEventLike<SentryEvent> {
SentryStackTrace? get stacktrace =>
exceptions?.firstWhereOrNull((e) => e.stackTrace != null)?.stackTrace ??
threads?.firstWhereOrNull((t) => t.stacktrace != null)?.stacktrace;

@override
Future<dynamic> asPayload() => Future.value(toJson());
}
8 changes: 5 additions & 3 deletions dart/lib/src/sentry_attachment/sentry_attachment.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import 'dart:async';
import 'dart:typed_data';

import '../protocol/sentry_view_hierarchy.dart';
import '../utils.dart';
import '../../sentry.dart';

// https://develop.sentry.dev/sdk/features/#attachments
// https://develop.sentry.dev/sdk/envelopes/#attachment

typedef ContentLoader = FutureOr<Uint8List> Function();

/// Arbitrary content which gets attached to an event.
class SentryAttachment {
class SentryAttachment implements SentryEnvelopeItemPayload {
/// Standard attachment without special meaning.
static const String typeAttachmentDefault = 'event.attachment';

Expand Down Expand Up @@ -122,4 +121,7 @@ class SentryAttachment {
/// If true, attachment should be added to every transaction.
/// Defaults to false.
final bool addToTransactions;

@override
Future<dynamic> asPayload() async => await bytes;
}
8 changes: 6 additions & 2 deletions dart/lib/src/sentry_envelope_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import 'sentry_item_type.dart';
import 'sentry_user_feedback.dart';
import 'utils.dart';

abstract class SentryEnvelopeItemPayload {
Future<dynamic> asPayload();
}

/// Item holding header information and JSON encoded data.
class SentryEnvelopeItem {
/// The original, non-encoded object, used when direct access to the source data is needed.
Object? originalObject;
SentryEnvelopeItemPayload? originalObject;

SentryEnvelopeItem(this.header, this.dataFactory, {this.originalObject});

Expand Down Expand Up @@ -102,7 +106,7 @@ class SentryEnvelopeItem {
return SentryEnvelopeItem(
header,
dataFactory,
originalObject: buckets,
originalObject: null,
);
}

Expand Down
7 changes: 5 additions & 2 deletions dart/lib/src/sentry_user_feedback.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'package:meta/meta.dart';

import 'protocol.dart';
import '../sentry.dart';
import 'protocol/access_aware_map.dart';

@Deprecated('Will be removed in a future version. Use [SentryFeedback] instead')
class SentryUserFeedback {
class SentryUserFeedback implements SentryEnvelopeItemPayload {
SentryUserFeedback({
required this.eventId,
this.name,
Expand Down Expand Up @@ -66,4 +66,7 @@ class SentryUserFeedback {
unknown: unknown,
);
}

@override
Future<dynamic> asPayload() => Future.value(toJson());
}
2 changes: 1 addition & 1 deletion dart/lib/src/version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
library version;

/// The SDK version reported to Sentry.io in the submitted events.
const String sdkVersion = '8.12.0-beta.2';
const String sdkVersion = '8.12.0';

String sdkName(bool isWeb) => isWeb ? _browserSdkName : _ioSdkName;

Expand Down
88 changes: 82 additions & 6 deletions flutter/example/integration_test/web_sdk_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
@TestOn('browser')
library flutter_test;

import 'dart:async';
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';
Expand All @@ -17,7 +19,30 @@ import 'utils.dart';
external JSObject get globalThis;

@JS('Sentry.getClient')
external JSObject? _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;
external JSArray get defaultIntegrations;
}

void main() {
group('Web SDK Integration', () {
Expand All @@ -37,16 +62,67 @@ void main() {
expect(globalThis['Sentry'], isNotNull);

final client = _getClient()!;
final options = client.callMethod('getOptions'.toJS)! as JSObject;
final options = client.getOptions();

final dsn = options.dsn.toDart;
final defaultIntegrations = options.defaultIntegrations.toDart;

final dsn = options.getProperty('dsn'.toJS).toString();
final defaultIntegrations = options
.getProperty('defaultIntegrations'.toJS)
.dartify() as List<Object?>;
await Sentry.captureException(Exception('test'));

expect(dsn, fakeDsn);
expect(defaultIntegrations, isEmpty);
});

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

await restoreFlutterOnErrorAfter(() async {
await SentryFlutter.init((options) {
options.enableSentryJs = true;
options.dsn = app.exampleDsn;
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 = item[1] 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', () {
Expand Down
4 changes: 4 additions & 0 deletions flutter/lib/src/integrations/web_sdk_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:sentry/sentry.dart';

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

Expand Down Expand Up @@ -38,6 +39,9 @@ class WebSdkIntegration implements Integration<SentryFlutterOptions> {
: productionScripts;
await _scriptLoader.loadWebSdk(scripts);
await _web.init(hub);
if (_web.supportsCaptureEnvelope) {
options.transport = JavascriptTransport(_web, options);
}
options.sdk.addIntegration(name);
} catch (exception, stackTrace) {
options.logger(
Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/native/c/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
throw UnsupportedError('$SentryNative.captureEnvelope() is not supported');
}

@override

Check warning on line 107 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L107

Added line #L107 was not covered by tests
FutureOr<void> captureEnvelopeObject(SentryEnvelope envelope) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a particular reason why it's called captureEnvelopeObject instead of just captureEnvelope?

Copy link
Contributor Author

@buenaflor buenaflor Jan 9, 2025

Choose a reason for hiding this comment

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

captureEnvelope already exists which only takes Uint8List and dart sadly doesnt support method overloading, we could theoretically use captureEnvelope but then we would have to convert it back to an envelope object which is unnecessary so that's why I added another captureEnvelope that is supposed to use the actual envelope object and not any other data structure

Copy link
Contributor Author

@buenaflor buenaflor Jan 9, 2025

Choose a reason for hiding this comment

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

other options is to just add another parameter SentryEnvelope envelope to the existing captureEnvelope but imo I'd prefer to have a second function since we dont have union types in dart, maybe in Dart 3 with sealed classes

throw UnsupportedError("Not supported on this platform");

Check warning on line 109 in flutter/lib/src/native/c/sentry_native.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/c/sentry_native.dart#L109

Added line #L109 was not covered by tests
}

@override
FutureOr<void> beginNativeFrames() {}

Expand Down
2 changes: 2 additions & 0 deletions flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ abstract class SentryNativeBinding {
FutureOr<void> captureEnvelope(
Uint8List envelopeData, bool containsUnhandledException);

FutureOr<void> captureEnvelopeObject(SentryEnvelope envelope);

FutureOr<void> beginNativeFrames();

FutureOr<NativeFrames?> endNativeFrames(SentryId id);
Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
'captureEnvelope', [envelopeData, containsUnhandledException]);
}

@override

Check warning on line 100 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L100

Added line #L100 was not covered by tests
FutureOr<void> captureEnvelopeObject(SentryEnvelope envelope) {
throw UnsupportedError("Not supported on this platform");

Check warning on line 102 in flutter/lib/src/native/sentry_native_channel.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/native/sentry_native_channel.dart#L102

Added line #L102 was not covered by tests
}

@override
bool get supportsLoadContexts => true;

Expand Down
7 changes: 6 additions & 1 deletion flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ mixin SentryFlutter {
// Not all platforms have a native integration.
if (_native != null) {
if (_native!.supportsCaptureEnvelope) {
options.transport = FileSystemTransport(_native!, options);
// Sentry's native web integration is only enabled when enableSentryJs=true.
// Transport configuration happens in web_integration because the configuration
// options aren't available until after the options callback executes.
if (!options.platformChecker.isWeb) {
options.transport = FileSystemTransport(_native!, options);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to set options.transport = JavascriptTransport(_web, options); in the else path here? Then i'd would be in the same place for both.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's not possible here since this is happening within SentryFlutter.init where the option is not yet evaluated and we only use the JS SDK if enableSentryJs = true (it's false by default) so the options callback is not evaluated and it will always be false here

I added the comment above that, maybe the comment can be improved to clarify this better

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this will be possible for v9 where we use the JS SDK by default

}
if (!options.platformChecker.isWeb) {
options.addScopeObserver(NativeScopeObserver(_native!));
Expand Down
9 changes: 9 additions & 0 deletions flutter/lib/src/web/html_sentry_js_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ class HtmlSentryJsBinding implements SentryJsBinding {
HtmlSentryJsBinding({JsObject? sentry}) : _sentry = sentry;

JsObject? _sentry;
dynamic _client;

@override
void init(Map<String, dynamic> options) {
_sentry ??= context['Sentry'] as JsObject;
_sentry!.callMethod('init', [JsObject.jsify(options)]);
_client = _sentry!.callMethod('getClient');
}

@override
Expand All @@ -25,4 +27,11 @@ class HtmlSentryJsBinding implements SentryJsBinding {
context['Sentry'] = null;
}
}

@override
void captureEnvelope(List<Object> envelope) {
if (_client != null) {
_client.callMethod('sendEnvelope', [JsObject.jsify(envelope)]);
}
}
}
26 changes: 26 additions & 0 deletions flutter/lib/src/web/javascript_transport.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import '../../sentry_flutter.dart';
import '../native/sentry_native_binding.dart';

class JavascriptTransport implements Transport {
JavascriptTransport(this._binding, this._options);

final SentryFlutterOptions _options;
final SentryNativeBinding _binding;

@override
Future<SentryId?> send(SentryEnvelope envelope) async {
try {
await _binding.captureEnvelopeObject(envelope);
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'Failed to send envelope',
exception: exception,
stackTrace: stackTrace,
);
return Future.value(SentryId.empty());
}

return envelope.header.eventId;
}
}
3 changes: 3 additions & 0 deletions flutter/lib/src/web/noop_sentry_js_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ class NoOpSentryJsBinding implements SentryJsBinding {

@override
void close() {}

@override
void captureEnvelope(List<Object> envelope) {}
}
1 change: 1 addition & 0 deletions flutter/lib/src/web/sentry_js_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export 'noop_sentry_js_binding.dart'
abstract class SentryJsBinding {
void init(Map<String, dynamic> options);
void close();
void captureEnvelope(List<Object> envelope);
}
Loading
Loading