Skip to content

Commit 5002a1f

Browse files
authored
feat(web): load injected debug ids (#2917)
* add initial impl * Update * Update mocks * Formatting * Update searching for debug id * Update * Update * Revert main * Improve readability * Return unmodifiable * Update CHANGELOG * Update web_load_debug_images_integration.dart * Update * Improve * Update naming * Add additional test * Update test * Update debug id impl * Update * Update * Rename * Fix analyze * Fix log * Fix test * update * Fix test * Formatting
1 parent bc82442 commit 5002a1f

16 files changed

+321
-17
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Features
66

7+
- Flutter Web: add debug ids to events ([#2917](https://github.com/getsentry/sentry-dart/pull/2917))
8+
- This allows support for symbolication based on [debug ids](https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/debug-ids/)
9+
- This only works if you use the Sentry Dart Plugin version `3.0.0` or higher
710
- Improved TTID/TTFD API ([#2866](https://github.com/getsentry/sentry-dart/pull/2866))
811
- This improves the stability and consistency of TTFD reporting by introducing new APIs
912
```dart

flutter/lib/src/integrations/integrations.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export 'debug_print_integration.dart';
22
export 'flutter_error_integration.dart';
33
export 'load_contexts_integration.dart';
4-
export 'load_native_debug_images_integration.dart';
4+
export 'load_debug_images_integration.dart';
55
export 'load_release_integration.dart';
66
export 'native_app_start_integration.dart';
77
export 'on_error_integration.dart';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export 'native_load_debug_images_integration.dart'
2+
if (dart.library.js_interop) 'web_load_debug_images_integration.dart';

flutter/lib/src/integrations/load_native_debug_images_integration.dart renamed to flutter/lib/src/integrations/native_load_debug_images_integration.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ import 'package:sentry/src/load_dart_debug_images_integration.dart';
77
import '../native/sentry_native_binding.dart';
88
import '../sentry_flutter_options.dart';
99

10+
Integration<SentryFlutterOptions> createLoadDebugImagesIntegration(
11+
SentryNativeBinding native) {
12+
return LoadNativeDebugImagesIntegration(native);
13+
}
14+
1015
/// Loads the native debug image list from the native SDKs for stack trace symbolication.
1116
class LoadNativeDebugImagesIntegration
1217
extends Integration<SentryFlutterOptions> {
1318
final SentryNativeBinding _native;
14-
static const integrationName = 'LoadNativeDebugImagesIntegration';
19+
static const integrationName = 'LoadNativeDebugImages';
1520

1621
LoadNativeDebugImagesIntegration(this._native);
1722

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import 'dart:async';
2+
3+
import 'package:sentry/sentry.dart';
4+
5+
import '../native/sentry_native_binding.dart';
6+
import '../sentry_flutter_options.dart';
7+
8+
Integration<SentryFlutterOptions> createLoadDebugImagesIntegration(
9+
SentryNativeBinding native) {
10+
return LoadWebDebugImagesIntegration(native);
11+
}
12+
13+
/// Loads the debug id injected by Sentry tooling e.g Sentry Dart Plugin
14+
/// This is necessary for symbolication of minified js stacktraces via debug ids.
15+
class LoadWebDebugImagesIntegration extends Integration<SentryFlutterOptions> {
16+
final SentryNativeBinding _native;
17+
static const integrationName = 'LoadWebDebugImages';
18+
19+
LoadWebDebugImagesIntegration(this._native);
20+
21+
@override
22+
void call(Hub hub, SentryFlutterOptions options) {
23+
options.addEventProcessor(
24+
_LoadDebugIdEventProcessor(_native),
25+
);
26+
options.sdk.addIntegration(integrationName);
27+
}
28+
}
29+
30+
class _LoadDebugIdEventProcessor implements EventProcessor {
31+
_LoadDebugIdEventProcessor(this._native);
32+
33+
final SentryNativeBinding _native;
34+
35+
@override
36+
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
37+
// ignore: invalid_use_of_internal_member
38+
final stackTrace = event.stacktrace;
39+
if (stackTrace == null) {
40+
return event;
41+
}
42+
final debugImages = await _native.loadDebugImages(stackTrace);
43+
if (debugImages != null) {
44+
event.debugMeta = DebugMeta(images: debugImages);
45+
}
46+
return event;
47+
}
48+
}

flutter/lib/src/sentry_flutter.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,11 @@ mixin SentryFlutter {
181181
// We also need to call this before the native sdk integrations so release is properly propagated.
182182
integrations.add(LoadReleaseIntegration());
183183
integrations.add(createSdkIntegration(native));
184+
integrations.add(createLoadDebugImagesIntegration(native));
184185
if (!platform.isWeb) {
185186
if (native.supportsLoadContexts) {
186187
integrations.add(LoadContextsIntegration(native));
187188
}
188-
integrations.add(LoadNativeDebugImagesIntegration(native));
189189
integrations.add(FramesTrackingIntegration(native));
190190
integrations.add(
191191
NativeAppStartIntegration(

flutter/lib/src/web/noop_sentry_js_binding.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@ class NoOpSentryJsBinding implements SentryJsBinding {
3232

3333
@override
3434
void updateSession({int? errors, String? status}) {}
35+
36+
@override
37+
Map<String, String>? getFilenameToDebugIdMap() {
38+
return {};
39+
}
3540
}

flutter/lib/src/web/sentry_js_binding.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ abstract class SentryJsBinding {
1111
Map<dynamic, dynamic>? getSession();
1212
void updateSession({int? errors, String? status});
1313
void captureSession();
14-
14+
Map<String, String>? getFilenameToDebugIdMap();
1515
@visibleForTesting
1616
dynamic getJsOptions();
1717
}

flutter/lib/src/web/sentry_web.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22
import 'dart:typed_data';
33

4+
import 'package:collection/collection.dart';
45
// ignore: implementation_imports
56
import 'package:sentry/src/sentry_item_type.dart';
67

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

22+
void _log(String message) {
23+
_options.log(SentryLevel.info, logger: '$SentryWeb', message);
24+
}
25+
2126
void _logNotSupported(String operation) =>
22-
options.log(SentryLevel.debug, 'SentryWeb: $operation is not supported');
27+
_log('$operation is not supported');
2328

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

179184
@override
180185
FutureOr<List<DebugImage>?> loadDebugImages(SentryStackTrace stackTrace) {
181-
_logNotSupported('loading debug images');
186+
final debugIdMap = _binding.getFilenameToDebugIdMap();
187+
if (debugIdMap == null || debugIdMap.isEmpty) {
188+
_log('Could not find debug id in js source file.');
189+
return null;
190+
}
191+
192+
final frame = stackTrace.frames.firstWhereOrNull((frame) {
193+
return debugIdMap.containsKey(frame.absPath) ||
194+
debugIdMap.containsKey(frame.fileName);
195+
});
196+
if (frame == null) {
197+
_log('Could not find any frame with a matching debug id.');
198+
return null;
199+
}
200+
201+
final codeFile = frame.absPath ?? frame.fileName;
202+
final debugId = debugIdMap[codeFile];
203+
if (debugId != null) {
204+
return [
205+
DebugImage(
206+
debugId: debugId,
207+
type: 'sourcemap',
208+
codeFile: codeFile,
209+
),
210+
];
211+
}
212+
213+
_log('Could not match any frame against the debug id map.');
182214
return null;
183215
}
184216

flutter/lib/src/web/web_sentry_js_binding.dart

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'dart:js_interop';
22
import 'dart:js_interop_unsafe';
33

4-
import 'package:flutter/cupertino.dart';
4+
import 'package:meta/meta.dart';
55

66
import 'sentry_js_binding.dart';
77

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

1212
class WebSentryJsBinding implements SentryJsBinding {
1313
SentryJsClient? _client;
14+
JSObject? _options;
15+
final Map<String, String> _filenameToDebugIds = {};
16+
final Set<String> _debugIdsWithFilenames = {};
17+
18+
int _lastKeysCount = 0;
19+
20+
@visibleForTesting
21+
Map<String, String>? get filenameToDebugIds => _filenameToDebugIds;
1422

1523
@override
1624
void init(Map<String, dynamic> options) {
@@ -20,6 +28,7 @@ class WebSentryJsBinding implements SentryJsBinding {
2028
}
2129
_init(options.jsify());
2230
_client = SentryJsClient();
31+
_options = _client?.getOptions();
2332
}
2433

2534
@override
@@ -54,10 +63,10 @@ class WebSentryJsBinding implements SentryJsBinding {
5463

5564
@override
5665
void close() {
57-
final sentryProp = _globalThis.getProperty('Sentry'.toJS);
66+
final sentryProp = globalThis.getProperty('Sentry'.toJS);
5867
if (sentryProp != null) {
5968
_close();
60-
_globalThis['Sentry'] = null;
69+
globalThis['Sentry'] = null;
6170
}
6271
}
6372

@@ -93,6 +102,74 @@ class WebSentryJsBinding implements SentryJsBinding {
93102
return null;
94103
}
95104
}
105+
106+
@override
107+
Map<String, String>? getFilenameToDebugIdMap() {
108+
final options = _options;
109+
if (options == null) {
110+
return null;
111+
}
112+
113+
final debugIdMap =
114+
globalThis['_sentryDebugIds'].dartify() as Map<dynamic, dynamic>?;
115+
if (debugIdMap == null) {
116+
return null;
117+
}
118+
119+
if (debugIdMap.keys.length != _lastKeysCount) {
120+
_buildFilenameToDebugIdMap(
121+
debugIdMap,
122+
options,
123+
);
124+
_lastKeysCount = debugIdMap.keys.length;
125+
}
126+
127+
return Map.unmodifiable(_filenameToDebugIds);
128+
}
129+
130+
void _buildFilenameToDebugIdMap(
131+
Map<dynamic, dynamic> debugIdMap,
132+
JSObject options,
133+
) {
134+
final stackParser = _stackParser(options);
135+
if (stackParser == null) {
136+
return;
137+
}
138+
139+
for (final debugIdMapEntry in debugIdMap.entries) {
140+
final String stackKeyStr = debugIdMapEntry.key.toString();
141+
final String debugIdStr = debugIdMapEntry.value.toString();
142+
143+
final debugIdHasCachedFilename =
144+
_debugIdsWithFilenames.contains(debugIdStr);
145+
146+
if (!debugIdHasCachedFilename) {
147+
final parsedStack = stackParser
148+
.callAsFunction(options, stackKeyStr.toJS)
149+
.dartify() as List<dynamic>?;
150+
151+
if (parsedStack == null) continue;
152+
153+
for (final stackFrame in parsedStack) {
154+
final stackFrameMap = stackFrame as Map<dynamic, dynamic>;
155+
final filename = stackFrameMap['filename']?.toString();
156+
if (filename != null) {
157+
_filenameToDebugIds[filename] = debugIdStr;
158+
_debugIdsWithFilenames.add(debugIdStr);
159+
break;
160+
}
161+
}
162+
}
163+
}
164+
}
165+
166+
JSFunction? _stackParser(JSObject options) {
167+
final parser = options['stackParser'];
168+
if (parser != null && parser.isA<JSFunction>()) {
169+
return parser as JSFunction;
170+
}
171+
return null;
172+
}
96173
}
97174

98175
@JS('Sentry.init')
@@ -136,4 +213,5 @@ external JSObject _globalHandlersIntegration();
136213
external JSObject _dedupeIntegration();
137214

138215
@JS('globalThis')
139-
external JSObject get _globalThis;
216+
@internal
217+
external JSObject get globalThis;

0 commit comments

Comments
 (0)