Skip to content

feat(web): load injected debug ids #2917

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Features

- [Flutter Web]: add debug ids to events ([#2917](https://github.com/getsentry/sentry-dart/pull/2917))
- This allows support for symbolication based on [debug ids](https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/debug-ids/)

## 9.0.0-RC

### Various fixes & improvements
Expand Down
2 changes: 1 addition & 1 deletion flutter/lib/src/integrations/integrations.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export 'debug_print_integration.dart';
export 'flutter_error_integration.dart';
export 'load_contexts_integration.dart';
export 'load_native_debug_images_integration.dart';
export 'load_debug_images_integration.dart';
export 'load_release_integration.dart';
export 'native_app_start_integration.dart';
export 'on_error_integration.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'native_load_debug_images_integration.dart'
if (dart.library.js_interop) 'web_load_debug_images_integration.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import 'package:sentry/src/load_dart_debug_images_integration.dart';
import '../native/sentry_native_binding.dart';
import '../sentry_flutter_options.dart';

Integration<SentryFlutterOptions> createLoadDebugImagesIntegration(
SentryNativeBinding native) {
return LoadNativeDebugImagesIntegration(native);
}

/// Loads the native debug image list from the native SDKs for stack trace symbolication.
class LoadNativeDebugImagesIntegration
extends Integration<SentryFlutterOptions> {
final SentryNativeBinding _native;
static const integrationName = 'LoadNativeDebugImagesIntegration';
static const integrationName = 'LoadNativeDebugImages';

LoadNativeDebugImagesIntegration(this._native);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'dart:async';

import 'package:sentry/sentry.dart';

import '../native/sentry_native_binding.dart';
import '../sentry_flutter_options.dart';

Integration<SentryFlutterOptions> createLoadDebugImagesIntegration(
SentryNativeBinding native) {
return LoadNativeDebugImagesIntegration(native);
}

/// Loads the debug id injected by Sentry tooling e.g Sentry Dart Plugin
/// This is necessary for symbolication of minified js stacktraces via debug ids.
class LoadNativeDebugImagesIntegration
extends Integration<SentryFlutterOptions> {
final SentryNativeBinding _native;
static const integrationName = 'LoadWebDebugImages';

LoadNativeDebugImagesIntegration(this._native);

@override
void call(Hub hub, SentryFlutterOptions options) {
options.addEventProcessor(
_LoadDebugIdEventProcessor(_native),
);
options.sdk.addIntegration(integrationName);
}
}

class _LoadDebugIdEventProcessor implements EventProcessor {
_LoadDebugIdEventProcessor(this._native);

final SentryNativeBinding _native;

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
// ignore: invalid_use_of_internal_member
final stackTrace = event.stacktrace;
if (stackTrace == null) {
return event;
}
final debugImages = await _native.loadDebugImages(stackTrace);
if (debugImages != null) {
event.debugMeta = DebugMeta(images: debugImages);
}
return event;
}
}
2 changes: 1 addition & 1 deletion flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,11 @@ mixin SentryFlutter {
// We also need to call this before the native sdk integrations so release is properly propagated.
integrations.add(LoadReleaseIntegration());
integrations.add(createSdkIntegration(native));
integrations.add(createLoadDebugImagesIntegration(native));
if (!platform.isWeb) {
if (native.supportsLoadContexts) {
integrations.add(LoadContextsIntegration(native));
}
integrations.add(LoadNativeDebugImagesIntegration(native));
integrations.add(FramesTrackingIntegration(native));
integrations.add(
NativeAppStartIntegration(
Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/web/noop_sentry_js_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ class NoOpSentryJsBinding implements SentryJsBinding {

@override
void updateSession({int? errors, String? status}) {}

@override
Map<String, String>? getFilenameToDebugIdMap() {
return {};
}
}
2 changes: 1 addition & 1 deletion flutter/lib/src/web/sentry_js_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ abstract class SentryJsBinding {
Map<dynamic, dynamic>? getSession();
void updateSession({int? errors, String? status});
void captureSession();

Map<String, String>? getFilenameToDebugIdMap();
@visibleForTesting
dynamic getJsOptions();
}
Expand Down
38 changes: 35 additions & 3 deletions flutter/lib/src/web/sentry_web.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:collection/collection.dart';
// ignore: implementation_imports
import 'package:sentry/src/sentry_item_type.dart';

Expand All @@ -18,8 +19,12 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding {
final SentryJsBinding _binding;
final SentryFlutterOptions _options;

void _logNotSupported(String operation) => options.logger(
SentryLevel.debug, 'SentryWeb: $operation is not supported');
void _log(String message) {
_options.logger(SentryLevel.info, logger: '$SentryWeb', message);
}

void _logNotSupported(String operation) =>
_log('$operation is not supported');

@override
FutureOr<void> init(Hub hub) {
Expand Down Expand Up @@ -178,7 +183,34 @@ class SentryWeb with SentryNativeSafeInvoker implements SentryNativeBinding {

@override
FutureOr<List<DebugImage>?> loadDebugImages(SentryStackTrace stackTrace) {
_logNotSupported('loading debug images');
final debugIdMap = _binding.getFilenameToDebugIdMap();
if (debugIdMap == null || debugIdMap.isEmpty) {
_log('Could not find debug id in js source file.');
return null;
}

final frame = stackTrace.frames.firstWhereOrNull((frame) {
return debugIdMap.containsKey(frame.absPath) ||
debugIdMap.containsKey(frame.fileName);
});
if (frame == null) {
_log('Could not find any frame with a matching debug id.');
return null;
}

final codeFile = frame.absPath ?? frame.fileName;
final debugId = debugIdMap[codeFile];
if (debugId != null) {
return [
DebugImage(
debugId: debugId,
type: 'sourcemap',
codeFile: codeFile,
),
];
}

_log('Could not match any frame against the debug id map.');
return null;
}

Expand Down
86 changes: 82 additions & 4 deletions flutter/lib/src/web/web_sentry_js_binding.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

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

import 'sentry_js_binding.dart';

Expand All @@ -11,6 +11,14 @@ SentryJsBinding createJsBinding() {

class WebSentryJsBinding implements SentryJsBinding {
SentryJsClient? _client;
JSObject? _options;
final Map<String, String> _filenameToDebugIds = {};
final Set<String> _debugIdsWithFilenames = {};

int _lastKeysCount = 0;

@visibleForTesting
Map<String, String>? get filenameToDebugIds => _filenameToDebugIds;

@override
void init(Map<String, dynamic> options) {
Expand All @@ -20,6 +28,7 @@ class WebSentryJsBinding implements SentryJsBinding {
}
_init(options.jsify());
_client = SentryJsClient();
_options = _client?.getOptions();
}

@override
Expand Down Expand Up @@ -54,10 +63,10 @@ class WebSentryJsBinding implements SentryJsBinding {

@override
void close() {
final sentryProp = _globalThis.getProperty('Sentry'.toJS);
final sentryProp = globalThis.getProperty('Sentry'.toJS);
if (sentryProp != null) {
_close();
_globalThis['Sentry'] = null;
globalThis['Sentry'] = null;
}
}

Expand Down Expand Up @@ -93,6 +102,74 @@ class WebSentryJsBinding implements SentryJsBinding {
return null;
}
}

@override
Map<String, String>? getFilenameToDebugIdMap() {
final options = _options;
if (options == null) {
return null;
}

final debugIdMap =
globalThis['_sentryDebugIds'].dartify() as Map<dynamic, dynamic>?;
if (debugIdMap == null) {
return null;
}

if (debugIdMap.keys.length != _lastKeysCount) {
_buildFilenameToDebugIdMap(
debugIdMap,
options,
);
_lastKeysCount = debugIdMap.keys.length;
}

return Map.unmodifiable(_filenameToDebugIds);
}

void _buildFilenameToDebugIdMap(
Map<dynamic, dynamic> debugIdMap,
JSObject options,
) {
final stackParser = _stackParser(options);
if (stackParser == null) {
return;
}

for (final debugIdMapEntry in debugIdMap.entries) {
final String stackKeyStr = debugIdMapEntry.key.toString();
final String debugIdStr = debugIdMapEntry.value.toString();

final debugIdHasCachedFilename =
_debugIdsWithFilenames.contains(debugIdStr);

if (!debugIdHasCachedFilename) {
final parsedStack = stackParser
.callAsFunction(options, stackKeyStr.toJS)
.dartify() as List<dynamic>?;

if (parsedStack == null) continue;

for (final stackFrame in parsedStack) {
final stackFrameMap = stackFrame as Map<dynamic, dynamic>;
final filename = stackFrameMap['filename']?.toString();
if (filename != null) {
_filenameToDebugIds[filename] = debugIdStr;
_debugIdsWithFilenames.add(debugIdStr);
break;
}
}
}
}
}

JSFunction? _stackParser(JSObject options) {
final parser = options['stackParser'];
if (parser != null && parser.isA<JSFunction>()) {
return parser as JSFunction;
}
return null;
}
}

@JS('Sentry.init')
Expand Down Expand Up @@ -136,4 +213,5 @@ external JSObject _globalHandlersIntegration();
external JSObject _dedupeIntegration();

@JS('globalThis')
external JSObject get _globalThis;
@internal
external JSObject get globalThis;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ library;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/integrations/load_native_debug_images_integration.dart';
import 'package:sentry_flutter/src/integrations/native_load_debug_images_integration.dart';

import 'fixture.dart';

Expand Down
11 changes: 10 additions & 1 deletion flutter/test/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,28 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:sentry/src/platform/platform.dart';
import 'package:sentry/src/sentry_tracer.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/frames_tracking/sentry_delayed_frames_tracker.dart';
import 'package:sentry_flutter/src/native/sentry_native_binding.dart';
import 'package:sentry_flutter/src/renderer/renderer.dart';
import 'package:sentry_flutter/src/web/sentry_js_binding.dart';
import 'package:sentry/src/platform/platform.dart';

import 'mocks.mocks.dart';
import 'no_such_method_provider.dart';

const fakeDsn = 'https://[email protected]/1234567';
const fakeProguardUuid = '3457d982-65ef-576d-a6ad-65b5f30f49a5';
final _firstFrame =
'''Error at chrome-extension://aeblfdkhhhdcdjpifhhbdiojplfjncoa/inline/injected/webauthn-listeners.js:2:127
at chrome-extension://aeblfdkhhhdcdjpifhhbdiojplfjncoa/inline/injected/webauthn-listeners.js:2:260
''';
final _secondFrame = '''Error at http://127.0.0.1:8080/main.dart.js:2:169
at http://127.0.0.1:8080/main.dart.js:2:304''';
// We wanna assert that the second frame is the correct debug id match
final debugIdMap = {_firstFrame: 'whatever debug id', _secondFrame: debugId};
final debugId = '82cc8a97-04c5-5e1e-b98d-bb3e647208e6';

SentryFlutterOptions defaultTestOptions(
{Platform? platform, RuntimeChecker? checker}) {
Expand Down
6 changes: 6 additions & 0 deletions flutter/test/mocks.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3004,6 +3004,12 @@ class MockHub extends _i1.Mock implements _i2.Hub {
),
) as _i2.ISentrySpan);

@override
void generateNewTraceId() => super.noSuchMethod(
Invocation.method(#generateNewTraceId, []),
returnValueForMissingStub: null,
);

@override
_i11.Future<_i2.SentryId> captureTransaction(
_i2.SentryTransaction? transaction, {
Expand Down
Loading
Loading