From bfd77cdd4add4fe27d0de10da1a88b03237bca30 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 17:22:35 +0200 Subject: [PATCH 01/88] Initial impl --- lib/sentry_dart_plugin.dart | 68 +++-------- lib/src/utils/flutter_debug_files.dart | 159 +++++++++++++++++++++++++ test/flutter_debug_files_test.dart | 159 +++++++++++++++++++++++++ 3 files changed, 336 insertions(+), 50 deletions(-) create mode 100644 lib/src/utils/flutter_debug_files.dart create mode 100644 test/flutter_debug_files_test.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index f8c0ff5a..915fc22d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -5,6 +5,7 @@ import 'package:process/process.dart'; import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; +import 'src/utils/flutter_debug_files.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -97,61 +98,28 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - Log.taskCompleted(taskName); - } - - Stream _enumerateDebugSymbolPaths(FileSystem fs) async* { - final buildDir = _configuration.buildFilesFolder; - final projectRoot = fs.currentDirectory.path; - - // Android (apk, appbundle) - yield '$buildDir/app/outputs'; - yield '$buildDir/app/intermediates'; - - // Windows - for (final subdir in ['', '/x64', '/arm64']) { - yield '$buildDir/windows$subdir/runner/Release'; + final all = await _findFlutterRelevantDebugFilePaths(); + for (final path in all) { + print('Found path: $path'); } - // TODO we should delete this once we have windows symbols collected automatically. - // Related to https://github.com/getsentry/sentry-dart-plugin/issues/173 - yield 'windows/flutter/ephemeral/flutter_windows.dll.pdb'; - // Linux - for (final subdir in ['/x64', '/arm64']) { - yield '$buildDir/linux$subdir/release/bundle'; - } - - // macOS - yield '$buildDir/macos/Build/Products/Release'; - - // macOS (macOS-framework) - yield '$buildDir/macos/framework/Release'; - - // iOS - yield '$buildDir/ios/iphoneos/Runner.app'; - if (await fs.directory('$buildDir/ios').exists()) { - final regexp = RegExp(r'^Release(-.*)?-iphoneos$'); - yield* fs - .directory('$buildDir/ios') - .list() - .where((v) => regexp.hasMatch(v.basename)) - .map((e) => e.path); - } - - // iOS (ipa) - yield '$buildDir/ios/archive'; - - // iOS (ios-framework) - yield '$buildDir/ios/framework/Release'; + Log.taskCompleted(taskName); + } - // iOS in Fastlane - if (projectRoot == '/') { - yield 'ios/build'; - } else { - yield '$projectRoot/ios/build'; - } + /// Internal helper to discover Flutter-relevant debug files without altering + /// the current upload behavior. Intended for future use. + // ignore: unused_element + Future> _findFlutterRelevantDebugFilePaths() async { + final fs = injector.get(); + return await findFlutterRelevantDebugFilePaths( + fs: fs, + config: _configuration, + ); } + Stream _enumerateDebugSymbolPaths(FileSystem fs) => + enumerateDebugSearchRoots(fs: fs, config: _configuration); + Future> _enumerateSymbolFiles() async { final result = {}; final fs = injector.get(); diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart new file mode 100644 index 00000000..f649effa --- /dev/null +++ b/lib/src/utils/flutter_debug_files.dart @@ -0,0 +1,159 @@ +import 'package:file/file.dart'; + +import '../configuration.dart'; + +/// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS). +/// +/// Task 1: Provide the public API surface only. Discovery logic will be added +/// in subsequent tasks to enumerate Android `.symbols` files and Apple Mach-O +/// files within `.dSYM` bundles. The function is expected to return absolute, +/// de-duplicated paths. +Future> findFlutterRelevantDebugFilePaths({ + required FileSystem fs, + required Configuration config, +}) async { + final Set foundPaths = {}; + + Future collectAndroidSymbolsUnder(String rootPath) async { + if (rootPath.isEmpty) return; + + final directory = fs.directory(rootPath); + if (await directory.exists()) { + await for (final entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(fs.file(entity.path).absolute.path); + } + } + return; + } + + final file = fs.file(rootPath); + if (await file.exists()) { + final String basename = fs.path.basename(file.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(file.absolute.path); + } + } + } + + // First, scan the configured symbols folder (if any) + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); + } + + // Backward compatibility: also scan build folder if different + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); + } + + // Then, scan all current search roots used by the plugin + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAndroidSymbolsUnder(root); + } + + Future collectAppleMachOUnder(String rootPath) async { + if (rootPath.isEmpty) return; + final dir = fs.directory(rootPath); + if (!await dir.exists()) return; + + await for (final entity in dir.list(recursive: true, followLinks: false)) { + if (entity is! Directory) continue; + final String basename = fs.path.basename(entity.path); + if (basename == 'App.framework.dSYM') { + final String machOPath = fs.path.join( + entity.path, + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } + } + } + + // Search under the build directory directly to catch common iOS layouts + await collectAppleMachOUnder(config.buildFilesFolder); + + // Search all known roots (includes Fastlane ios/build) + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAppleMachOUnder(root); + } + + return foundPaths; +} + +/// Enumerates the search roots used to discover native debug files, matching +/// the existing behavior used by the plugin when uploading debug files. +/// +/// This preserves current directories and files probed for: +/// - Android (apk, appbundle) +/// - Windows +/// - Linux +/// - macOS (app and framework) +/// - iOS (Runner.app, Release-*-iphoneos folders, archive, framework) +/// - iOS in Fastlane (ios/build) +Stream enumerateDebugSearchRoots({ + required FileSystem fs, + required Configuration config, +}) async* { + final String buildDir = config.buildFilesFolder; + final String projectRoot = fs.currentDirectory.path; + + // Android (apk, appbundle) + yield '$buildDir/app/outputs'; + yield '$buildDir/app/intermediates'; + + // Windows + for (final subdir in ['', '/x64', '/arm64']) { + yield '$buildDir/windows$subdir/runner/Release'; + } + // TODO: Consider removing once Windows symbols are collected automatically. + // Related to https://github.com/getsentry/sentry-dart-plugin/issues/173 + yield 'windows/flutter/ephemeral/flutter_windows.dll.pdb'; + + // Linux + for (final subdir in ['/x64', '/arm64']) { + yield '$buildDir/linux$subdir/release/bundle'; + } + + // macOS + yield '$buildDir/macos/Build/Products/Release'; + + // macOS (macOS-framework) + yield '$buildDir/macos/framework/Release'; + + // iOS + yield '$buildDir/ios/iphoneos/Runner.app'; + final iosDir = fs.directory('$buildDir/ios'); + if (await iosDir.exists()) { + final regexp = RegExp(r'^Release(-.*)?-iphoneos$'); + yield* iosDir + .list() + .where((entity) => regexp.hasMatch(fs.path.basename(entity.path))) + .map((entity) => entity.path); + } + + // iOS (ipa) + yield '$buildDir/ios/archive'; + + // iOS (ios-framework) + yield '$buildDir/ios/framework/Release'; + + // iOS in Fastlane + if (projectRoot == '/') { + yield 'ios/build'; + } else { + yield '$projectRoot/ios/build'; + } +} diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart new file mode 100644 index 00000000..3e0fbe96 --- /dev/null +++ b/test/flutter_debug_files_test.dart @@ -0,0 +1,159 @@ +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; +import 'package:sentry_dart_plugin/src/configuration.dart'; + +void main() { + group('findFlutterRelevantDebugFilePaths', () { + test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', + () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/work')..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/work/build'; + final symbolsDir = '/work/symbols'; + + // Android .symbols files + fs + .file('$symbolsDir/app.android-arm.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-arm64.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-x64.symbols') + .createSync(recursive: true); + + // Apple App.framework.dSYM Mach-O + final appDsymMachO = + '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(appDsymMachO).createSync(recursive: true); + + // Noise: other .dSYM bundles should be ignored + fs + .file( + '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect( + result, + containsAll([ + fs.path.normalize('/work/symbols/app.android-arm.symbols'), + fs.path.normalize('/work/symbols/app.android-arm64.symbols'), + fs.path.normalize('/work/symbols/app.android-x64.symbols'), + fs.path.normalize(appDsymMachO), + ])); + + // Ensure we did not include non-App.framework dSYMs + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + + // Ensure deduplication and absoluteness + expect(result.length, 4); + for (final p in result) { + expect(p.startsWith('/'), isTrue, + reason: 'path should be absolute: $p'); + } + }); + + test('finds App.framework.dSYM under Fastlane ios/build path', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/project') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/project/build'; + final symbolsDir = '/project/symbols'; + + // Fastlane path + final machO = + '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(machO).createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(machO))); + }); + + test('finds App.framework.dSYM in macOS build products', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/macosproj') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/macosproj/build'; + final symbolsDir = '/macosproj/symbols'; + + // macOS Products Release path + final macMachO = + '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(macMachO).createSync(recursive: true); + + // Noise: other dSYMs should be ignored + fs + .file( + '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(macMachO))); + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + }); + + test('returns empty set when no roots or symbols exist', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/empty') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/empty/build'; + final symbolsDir = '/empty/symbols'; + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, isEmpty); + }); + }); +} From 99dee07c4f599463f660ce6c0a81a5398267efa4 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 17:24:42 +0200 Subject: [PATCH 02/88] Initial impl --- lib/sentry_dart_plugin.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 915fc22d..c53a1b2c 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -98,10 +98,8 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final all = await _findFlutterRelevantDebugFilePaths(); - for (final path in all) { - print('Found path: $path'); - } + final _ = await _findFlutterRelevantDebugFilePaths(); + // TODO(buenaflor): upload these files with the mapping file Log.taskCompleted(taskName); } From 21ba340a8e59f2ac4693558d32114a2ad91201ab Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 18:29:29 +0200 Subject: [PATCH 03/88] Initial impl --- lib/sentry_dart_plugin.dart | 17 ++++++ lib/src/configuration.dart | 4 ++ lib/src/configuration_values.dart | 12 ++++ lib/src/utils/dart_symbol_map.dart | 65 +++++++++++++++++++++ test/dart_symbol_map_integration_test.dart | 62 ++++++++++++++++++++ test/dart_symbol_map_test.dart | 67 ++++++++++++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 lib/src/utils/dart_symbol_map.dart create mode 100644 test/dart_symbol_map_integration_test.dart create mode 100644 test/dart_symbol_map_test.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index c53a1b2c..46f83393 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -6,6 +6,7 @@ import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; +import 'src/utils/dart_symbol_map.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -101,6 +102,22 @@ class SentryDartPlugin { final _ = await _findFlutterRelevantDebugFilePaths(); // TODO(buenaflor): upload these files with the mapping file + // Attempt to resolve the Dart obfuscation map (explicit path only). + try { + final fs = injector.get(); + final symbolMapPath = + await findDartSymbolMapPath(fs: fs, config: _configuration); + if (symbolMapPath != null) { + Log.info('Found Dart obfuscation map at: $symbolMapPath'); + } else { + Log.warn( + "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Continuing without a Dart symbol map."); + } + } on Exception catch (e) { + Log.error(e.toString()); + throw ExitError(1); + } + Log.taskCompleted(taskName); } diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index e0723502..6aa51932 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -73,6 +73,9 @@ class Configuration { /// The directory passed to `--split-debug-info`, defaults to '.' late String symbolsFolder; + /// Explicit path to the Dart obfuscation map file. Optional. + late String? dartSymbolMapPath; + /// The URL prefix, defaults to null late String? urlPrefix; @@ -154,6 +157,7 @@ class Configuration { final webBuildPath = configValues.webBuildPath ?? 'web'; webBuildFilesFolder = _fs.path.join(buildFilesFolder, webBuildPath); symbolsFolder = configValues.symbolsPath ?? '.'; + dartSymbolMapPath = configValues.dartSymbolMapPath; project = configValues.project; // or env. var. SENTRY_PROJECT org = configValues.org; // or env. var. SENTRY_ORG diff --git a/lib/src/configuration_values.dart b/lib/src/configuration_values.dart index 083a4aff..68710c37 100644 --- a/lib/src/configuration_values.dart +++ b/lib/src/configuration_values.dart @@ -20,6 +20,7 @@ class ConfigurationValues { final String? buildPath; final String? webBuildPath; final String? symbolsPath; + final String? dartSymbolMapPath; final String? commits; final bool? ignoreMissing; final String? binDir; @@ -46,6 +47,7 @@ class ConfigurationValues { this.buildPath, this.webBuildPath, this.symbolsPath, + this.dartSymbolMapPath, this.commits, this.ignoreMissing, this.binDir, @@ -99,6 +101,7 @@ class ConfigurationValues { buildPath: sentryArguments['build_path'], webBuildPath: sentryArguments['web_build_path'], symbolsPath: sentryArguments['symbols_path'], + dartSymbolMapPath: sentryArguments['dart_symbol_map_path'], commits: sentryArguments['commits'], ignoreMissing: boolFromString(sentryArguments['ignore_missing']), binDir: sentryArguments['bin_dir'], @@ -136,6 +139,7 @@ class ConfigurationValues { buildPath: configReader.getString('build_path'), webBuildPath: configReader.getString('web_build_path'), symbolsPath: configReader.getString('symbols_path'), + dartSymbolMapPath: configReader.getString('dart_symbol_map_path'), commits: configReader.getString('commits'), ignoreMissing: configReader.getBool('ignore_missing'), binDir: configReader.getString('bin_dir'), @@ -161,10 +165,15 @@ class ConfigurationValues { if (envSentryCliCdnUrl?.isEmpty ?? false) { envSentryCliCdnUrl = null; } + String? envDartSymbolMapPath = environment['SENTRY_DART_SYMBOL_MAP_PATH']; + if (envDartSymbolMapPath?.isEmpty ?? false) { + envDartSymbolMapPath = null; + } return ConfigurationValues( release: envRelease, dist: envDist, sentryCliCdnUrl: envSentryCliCdnUrl, + dartSymbolMapPath: envDartSymbolMapPath, ); } @@ -191,6 +200,9 @@ class ConfigurationValues { buildPath: args.buildPath ?? file.buildPath, webBuildPath: args.webBuildPath ?? file.webBuildPath, symbolsPath: args.symbolsPath ?? file.symbolsPath, + dartSymbolMapPath: platformEnv.dartSymbolMapPath ?? + args.dartSymbolMapPath ?? + file.dartSymbolMapPath, commits: args.commits ?? file.commits, ignoreMissing: args.ignoreMissing ?? file.ignoreMissing, binDir: args.binDir ?? file.binDir, diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart new file mode 100644 index 00000000..f7ade187 --- /dev/null +++ b/lib/src/utils/dart_symbol_map.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import 'package:file/file.dart'; + +import '../configuration.dart'; + +/// Maximum Dart symbol map file size to consider during validation (20 MiB). +const int kMaxDartSymbolMapSizeBytes = 20 * 1024 * 1024; + +/// Validates whether the given [file] contains a Dart obfuscation map. +/// +/// A valid map is a JSON array of strings with even length and at least one +/// pair (length >= 2), e.g. ["MaterialApp", "ex", "Scaffold", "ey"]. +Future isValidDartSymbolMapFile(File file) async { + try { + final stat = await file.stat(); + if (stat.type != FileSystemEntityType.file) return false; + if (stat.size <= 0 || stat.size > kMaxDartSymbolMapSizeBytes) return false; + + final content = await file.readAsString(); + final trimmed = content.trim(); + if (trimmed.length < 2) return false; + if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return false; + + final dynamic decoded = jsonDecode(trimmed); + if (decoded is! List) return false; + if (decoded.isEmpty) return false; + if (decoded.length.isOdd) return false; + if (decoded.length < 2) return false; + for (final element in decoded) { + if (element is! String) return false; + } + return true; + } catch (_) { + return false; + } +} + +/// Attempts to resolve the Dart obfuscation map path. +/// +/// - If [config.dartSymbolMapPath] is provided, it must exist and be valid, +/// otherwise an [Exception] is thrown. On success, returns the absolute path. +/// - Otherwise, returns null (no scanning by default due to performance). +Future findDartSymbolMapPath({ + required FileSystem fs, + required Configuration config, +}) async { + final String? explicitPath = config.dartSymbolMapPath; + if (explicitPath != null && explicitPath.isNotEmpty) { + final file = fs.file(explicitPath); + if (!await file.exists()) { + throw Exception( + "Dart symbol map not found at provided path '$explicitPath'.", + ); + } + final isValid = await isValidDartSymbolMapFile(file); + if (!isValid) { + throw Exception( + "Provided 'dart_symbol_map_path' is not a valid Dart obfuscation map: '$explicitPath'.", + ); + } + return file.absolute.path; + } + return null; +} diff --git a/test/dart_symbol_map_integration_test.dart b/test/dart_symbol_map_integration_test.dart new file mode 100644 index 00000000..84488f64 --- /dev/null +++ b/test/dart_symbol_map_integration_test.dart @@ -0,0 +1,62 @@ +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; +import 'package:test/test.dart'; + +void main() { + late FileSystem fs; + late Configuration config; + + setUp(() { + fs = MemoryFileSystem.test(); + config = Configuration(); + // Ensure predictable build folder default for tests, though scanning is disabled. + config.buildFilesFolder = 'build'; + config.dartSymbolMapPath = null; + }); + + group('findDartSymbolMapPath (explicit path only, no scanning)', () { + test('returns absolute path when explicit path is valid', () async { + final file = fs.file('map.json') + ..createSync(recursive: true) + ..writeAsStringSync('["A","B"]'); + + config.dartSymbolMapPath = 'map.json'; + + final result = await findDartSymbolMapPath(fs: fs, config: config); + expect(result, equals(file.absolute.path)); + }); + + test('throws when explicit path is missing', () async { + config.dartSymbolMapPath = 'missing.json'; + expect( + () => findDartSymbolMapPath(fs: fs, config: config), + throwsA(isA()), + ); + }); + + test('throws when explicit path exists but is invalid', () async { + final file = fs.file('invalid.json') + ..createSync(recursive: true) + ..writeAsStringSync('[]'); // empty array is invalid + config.dartSymbolMapPath = file.path; + expect( + () => findDartSymbolMapPath(fs: fs, config: config), + throwsA(isA()), + ); + }); + + test('returns null when explicit path is not provided', () async { + // Place a valid map file somewhere under build to verify no scanning. + fs.directory('build/assets').createSync(recursive: true); + fs.file('build/assets/some_map') + ..createSync() + ..writeAsStringSync('["A","B"]'); + + config.dartSymbolMapPath = null; + final result = await findDartSymbolMapPath(fs: fs, config: config); + expect(result, isNull); + }); + }); +} diff --git a/test/dart_symbol_map_test.dart b/test/dart_symbol_map_test.dart new file mode 100644 index 00000000..511563e8 --- /dev/null +++ b/test/dart_symbol_map_test.dart @@ -0,0 +1,67 @@ +import 'dart:typed_data'; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; +import 'package:test/test.dart'; + +void main() { + late FileSystem fs; + + setUp(() { + fs = MemoryFileSystem.test(); + }); + + group('isValidDartSymbolMapFile', () { + test('valid when array of strings with even length (single pair)', + () async { + final file = fs.file('map.json')..writeAsStringSync('["A","B"]'); + expect(await isValidDartSymbolMapFile(file), isTrue); + }); + + test('valid when array has multiple pairs', () async { + final file = fs.file('map_multi.json') + ..writeAsStringSync('["MaterialApp","ex","Scaffold","ey"]'); + expect(await isValidDartSymbolMapFile(file), isTrue); + }); + + test('valid when trailing whitespace/newline present', () async { + final file = fs.file('map_ws.json')..writeAsStringSync('["A","B"]\n '); + expect(await isValidDartSymbolMapFile(file), isTrue); + }); + + test('invalid when empty array', () async { + final file = fs.file('empty.json')..writeAsStringSync('[]'); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + + test('invalid when odd number of elements', () async { + final file = fs.file('odd.json')..writeAsStringSync('["A"]'); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + + test('invalid when non-string element present', () async { + final file = fs.file('non_string.json')..writeAsStringSync('[1, "A"]'); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + + test('invalid when top-level JSON is not an array', () async { + final file = fs.file('not_array.json')..writeAsStringSync('{"a":"b"}'); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + + test('invalid when malformed JSON', () async { + final file = fs.file('malformed.json')..writeAsStringSync('['); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + + test('invalid when file larger than maximum threshold', () async { + final file = fs.file('too_big'); + // Create a file of size kMaxDartSymbolMapSizeBytes + 1 without valid JSON + // to trigger size-based rejection without decoding. + final tooBig = kMaxDartSymbolMapSizeBytes + 1; + await file.writeAsBytes(Uint8List(tooBig)); + expect(await isValidDartSymbolMapFile(file), isFalse); + }); + }); +} From 4449ef8dad6ece2f57de718311a6586782c6a91a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 12:23:02 +0200 Subject: [PATCH 04/88] Update --- lib/sentry_dart_plugin.dart | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index c53a1b2c..6ac92eb8 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -87,7 +87,8 @@ class SentryDartPlugin { _addWait(params); final fs = injector.get(); - final debugSymbolPaths = _enumerateDebugSymbolPaths(fs); + final debugSymbolPaths = + enumerateDebugSearchRoots(fs: fs, config: _configuration); await for (final path in debugSymbolPaths) { if (await fs.directory(path).exists() || await fs.file(path).exists()) { await _executeAndLog('Failed to upload symbols', [...params, path]); @@ -98,25 +99,14 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final _ = await _findFlutterRelevantDebugFilePaths(); - // TODO(buenaflor): upload these files with the mapping file - - Log.taskCompleted(taskName); - } - - /// Internal helper to discover Flutter-relevant debug files without altering - /// the current upload behavior. Intended for future use. - // ignore: unused_element - Future> _findFlutterRelevantDebugFilePaths() async { - final fs = injector.get(); - return await findFlutterRelevantDebugFilePaths( + final _ = await findFlutterRelevantDebugFilePaths( fs: fs, config: _configuration, ); - } + // TODO(buenaflor): in the follow up PR use these files with the dart symbol mapping file - Stream _enumerateDebugSymbolPaths(FileSystem fs) => - enumerateDebugSearchRoots(fs: fs, config: _configuration); + Log.taskCompleted(taskName); + } Future> _enumerateSymbolFiles() async { final result = {}; From 7fed2aa27a23233a1ffb6bc8ff55b0140d0abffb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 13:05:48 +0200 Subject: [PATCH 05/88] Update --- lib/src/utils/flutter_debug_files.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart index f649effa..7c821954 100644 --- a/lib/src/utils/flutter_debug_files.dart +++ b/lib/src/utils/flutter_debug_files.dart @@ -3,11 +3,7 @@ import 'package:file/file.dart'; import '../configuration.dart'; /// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS). -/// -/// Task 1: Provide the public API surface only. Discovery logic will be added -/// in subsequent tasks to enumerate Android `.symbols` files and Apple Mach-O -/// files within `.dSYM` bundles. The function is expected to return absolute, -/// de-duplicated paths. +/// TODO(buenaflor): in the follow-up PR this should be coupled together with the dart symbol map Future> findFlutterRelevantDebugFilePaths({ required FileSystem fs, required Configuration config, From 46729fd2d415d16dfc0057464b7d225f3db5546a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 13:11:37 +0200 Subject: [PATCH 06/88] Fix --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 43c33b6a..9a96a28c 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -47,7 +47,7 @@ jobs: distribution: 'temurin' java-version: '17' - - run: sudo apt-get install ninja-build libgtk-3-dev + - run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev if: runner.os == 'Linux' - run: (flutter --version)[0] | Out-File flutter.version From 3272add4c6d7f591bb4d7fecce9a4dd3c5eb390c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 14:56:52 +0200 Subject: [PATCH 07/88] Update --- lib/sentry_dart_plugin.dart | 92 ++++++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index b53556e0..45cd7999 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -15,6 +15,9 @@ import 'src/utils/log.dart'; class SentryDartPlugin { late Configuration _configuration; final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); + // Temporary guard while sentry-cli doesn't support the dart-symbol-map command yet. + // Flip to 'true' once the command is available in the bundled CLI version. + static const bool _dartSymbolMapUploadEnabled = false; /// SentryDartPlugin ctor. that inits the injectors SentryDartPlugin() { @@ -100,27 +103,8 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final _ = await findFlutterRelevantDebugFilePaths( - fs: fs, - config: _configuration, - ); - // TODO(buenaflor): upload these files with the mapping file - - // Attempt to resolve the Dart obfuscation map (explicit path only). - try { - final fs = injector.get(); - final symbolMapPath = - await findDartSymbolMapPath(fs: fs, config: _configuration); - if (symbolMapPath != null) { - Log.info('Found Dart obfuscation map at: $symbolMapPath'); - } else { - Log.warn( - "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Continuing without a Dart symbol map."); - } - } on Exception catch (e) { - Log.error(e.toString()); - throw ExitError(1); - } + // Attempt to upload the Dart symbol map for each relevant debug file (guarded, no-op for now). + await _tryUploadDartSymbolMapForDebugFiles(); Log.taskCompleted(taskName); } @@ -151,6 +135,72 @@ class SentryDartPlugin { return result; } + // Guarded implementation for uploading Dart symbol map alongside each relevant debug file. + // Currently no-ops until `_dartSymbolMapUploadEnabled` is flipped to true. -> Temporary guard. + Future _tryUploadDartSymbolMapForDebugFiles() async { + if (!_dartSymbolMapUploadEnabled) { + Log.info('Dart symbol map upload is disabled in this version. Skipping.'); + return; + } + + const taskName = 'uploading Dart symbol map(s)'; + Log.startingTask(taskName); + + try { + final fs = injector.get(); + + // Check for explicitly provided and valid Dart symbol map. + final symbolMapPath = + await findDartSymbolMapPath(fs: fs, config: _configuration); + if (symbolMapPath == null) { + Log.warn( + "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Skipping Dart symbol map uploads."); + Log.taskCompleted(taskName); + return; + } + + final debugFilePaths = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: _configuration, + ); + + if (debugFilePaths.isEmpty) { + Log.info( + 'No Flutter-relevant debug files found for Dart symbol map upload.'); + Log.taskCompleted(taskName); + return; + } + + for (final debugFilePath in debugFilePaths) { + // Accept both files and directories, mirroring current symbol search behavior. + final isFile = await fs.file(debugFilePath).exists(); + final isDir = await fs.directory(debugFilePath).exists(); + if (!isFile && !isDir) { + continue; + } + + final params = []; + _setUrlAndTokenAndLog(params); + params.add('dart-symbol-map'); + params.add('upload'); + _addOrgAndProject(params); + _addWait(params); + params.add(symbolMapPath); + params.add(debugFilePath); + + await _executeAndLog( + 'Failed to upload Dart symbol map for $debugFilePath', + params, + ); + } + + Log.taskCompleted(taskName); + } on Exception catch (e) { + Log.error('Dart symbol map upload failed: $e'); + throw ExitError(1); + } + } + List _baseCliParams({bool addReleases = false}) { final params = []; if (addReleases) { From a25d75affb5c98b9948797a3ebea9f1a1b973c06 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 14:58:23 +0200 Subject: [PATCH 08/88] Update --- lib/sentry_dart_plugin.dart | 79 +++++++++++++- lib/src/utils/dart_symbol_map.dart | 144 +++++++++++++++++++++++++ lib/src/utils/flutter_debug_files.dart | 87 --------------- test/flutter_debug_files_test.dart | 2 +- 4 files changed, 219 insertions(+), 93 deletions(-) create mode 100644 lib/src/utils/dart_symbol_map.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 6ac92eb8..21cd1de3 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -6,6 +6,7 @@ import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; +import 'src/utils/dart_symbol_map.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -14,6 +15,8 @@ import 'src/utils/log.dart'; class SentryDartPlugin { late Configuration _configuration; final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); + // Temporary feature flag: guarded no-op until sentry-cli supports Dart symbol map upload. + final bool _dartSymbolMapUploadEnabled = false; /// SentryDartPlugin ctor. that inits the injectors SentryDartPlugin() { @@ -99,11 +102,8 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final _ = await findFlutterRelevantDebugFilePaths( - fs: fs, - config: _configuration, - ); - // TODO(buenaflor): in the follow up PR use these files with the dart symbol mapping file + // Attempt (guarded) to upload Dart symbol map alongside relevant Flutter debug files. + await _tryUploadDartSymbolMapForDebugFiles(); Log.taskCompleted(taskName); } @@ -150,6 +150,75 @@ class SentryDartPlugin { return params; } + /// Guarded implementation for uploading Dart symbol map alongside each relevant debug file. + /// Currently a no-op until `_dartSymbolMapUploadEnabled` is flipped to true. + Future _tryUploadDartSymbolMapForDebugFiles() async { + if (!_dartSymbolMapUploadEnabled) { + Log.info('Dart symbol map upload is disabled in this version. Skipping.'); + return; + } + + const taskName = 'uploading Dart symbol map(s)'; + Log.startingTask(taskName); + + try { + final fs = injector.get(); + + // Check for explicitly provided and valid Dart symbol map (wire to config in a follow-up). + final symbolMapPath = await resolveDartSymbolMapPath( + fs: fs, + configuredPath: null, + ); + if (symbolMapPath == null) { + Log.warn( + "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Skipping Dart symbol map uploads.", + ); + Log.taskCompleted(taskName); + return; + } + + final debugFilePaths = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: _configuration, + ); + + if (debugFilePaths.isEmpty) { + Log.info( + 'No Flutter-relevant debug files found for Dart symbol map upload.'); + Log.taskCompleted(taskName); + return; + } + + for (final debugFilePath in debugFilePaths) { + // Accept both files and directories, mirroring current symbol search behavior. + final isFile = await fs.file(debugFilePath).exists(); + final isDir = await fs.directory(debugFilePath).exists(); + if (!isFile && !isDir) { + continue; + } + + final params = []; + _setUrlAndTokenAndLog(params); + params.add('dart-symbol-map'); + params.add('upload'); + _addOrgAndProject(params); + _addWait(params); + params.add(symbolMapPath); + params.add(debugFilePath); + + await _executeAndLog( + 'Failed to upload Dart symbol map for $debugFilePath', + params, + ); + } + + Log.taskCompleted(taskName); + } catch (e) { + Log.error('Dart symbol map upload failed: $e'); + Log.taskCompleted(taskName); + } + } + Future _executeNewRelease(String release) async { await _executeAndLog('Failed to create a new release', [..._releasesCliParams(), 'new', release]); diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart new file mode 100644 index 00000000..6467562b --- /dev/null +++ b/lib/src/utils/dart_symbol_map.dart @@ -0,0 +1,144 @@ +import 'dart:convert'; + +import 'package:file/file.dart'; + +import '../configuration.dart'; +import 'flutter_debug_files.dart'; + +/// Validates whether a file looks like a Dart obfuscation map. +/// +/// Expected minimal shape: top-level JSON array of strings with an even length +/// (pairs of original and obfuscated names). +Future isValidDartSymbolMapFile(File file) async { + try { + // Basic size guard: reject extremely large files to avoid excessive memory usage. + final stat = await file.stat(); + // ~20MB upper bound is generous for typical obfuscation maps. + const int maxBytes = 20 * 1024 * 1024; + if (stat.size > maxBytes) return false; + + final content = (await file.readAsString()).trim(); + if (!(content.startsWith('[') && content.endsWith(']'))) return false; + + final decoded = jsonDecode(content); + if (decoded is! List) return false; + if (decoded.isEmpty || decoded.length.isOdd) return false; + for (final item in decoded) { + if (item is! String) return false; + } + return true; + } catch (_) { + return false; + } +} + +/// If provided, validates and returns the absolute path to the Dart symbol map. +/// If not provided, returns null without scanning the filesystem. +Future resolveDartSymbolMapPath({ + required FileSystem fs, + String? configuredPath, +}) async { + if (configuredPath == null || configuredPath.isEmpty) return null; + + final file = fs.file(configuredPath); + if (!await file.exists()) { + throw StateError( + "Dart symbol map file not found at '$configuredPath'. Ensure the path is correct and the file exists.", + ); + } + + if (!await isValidDartSymbolMapFile(file)) { + throw StateError( + "Invalid Dart symbol map at '$configuredPath'. It must be a JSON array of strings with an even number of elements.", + ); + } + + return file.absolute.path; +} + +/// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS) +/// that should be paired with a Dart symbol map. +Future> findFlutterRelevantDebugFilePaths({ + required FileSystem fs, + required Configuration config, +}) async { + final Set foundPaths = {}; + + Future collectAndroidSymbolsUnder(String rootPath) async { + if (rootPath.isEmpty) return; + + final directory = fs.directory(rootPath); + if (await directory.exists()) { + await for (final entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(fs.file(entity.path).absolute.path); + } + } + return; + } + + final file = fs.file(rootPath); + if (await file.exists()) { + final String basename = fs.path.basename(file.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(file.absolute.path); + } + } + } + + // First, scan the configured symbols folder (if any) + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); + } + + // Backward compatibility: also scan build folder if different + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); + } + + // Then, scan all current search roots used by the plugin + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAndroidSymbolsUnder(root); + } + + Future collectAppleMachOUnder(String rootPath) async { + if (rootPath.isEmpty) return; + final dir = fs.directory(rootPath); + if (!await dir.exists()) return; + + await for (final entity in dir.list(recursive: true, followLinks: false)) { + if (entity is! Directory) continue; + final String basename = fs.path.basename(entity.path); + if (basename == 'App.framework.dSYM') { + final String machOPath = fs.path.join( + entity.path, + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } + } + } + + // Search under the build directory directly to catch common iOS layouts + await collectAppleMachOUnder(config.buildFilesFolder); + + // Search all known roots (includes Fastlane ios/build) + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAppleMachOUnder(root); + } + + return foundPaths; +} diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart index 7c821954..f7dfc9eb 100644 --- a/lib/src/utils/flutter_debug_files.dart +++ b/lib/src/utils/flutter_debug_files.dart @@ -2,93 +2,6 @@ import 'package:file/file.dart'; import '../configuration.dart'; -/// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS). -/// TODO(buenaflor): in the follow-up PR this should be coupled together with the dart symbol map -Future> findFlutterRelevantDebugFilePaths({ - required FileSystem fs, - required Configuration config, -}) async { - final Set foundPaths = {}; - - Future collectAndroidSymbolsUnder(String rootPath) async { - if (rootPath.isEmpty) return; - - final directory = fs.directory(rootPath); - if (await directory.exists()) { - await for (final entity - in directory.list(recursive: true, followLinks: false)) { - if (entity is! File) continue; - final String basename = fs.path.basename(entity.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin')) { - foundPaths.add(fs.file(entity.path).absolute.path); - } - } - return; - } - - final file = fs.file(rootPath); - if (await file.exists()) { - final String basename = fs.path.basename(file.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin')) { - foundPaths.add(file.absolute.path); - } - } - } - - // First, scan the configured symbols folder (if any) - if (config.symbolsFolder.isNotEmpty) { - await collectAndroidSymbolsUnder(config.symbolsFolder); - } - - // Backward compatibility: also scan build folder if different - if (config.buildFilesFolder != config.symbolsFolder) { - await collectAndroidSymbolsUnder(config.buildFilesFolder); - } - - // Then, scan all current search roots used by the plugin - await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { - await collectAndroidSymbolsUnder(root); - } - - Future collectAppleMachOUnder(String rootPath) async { - if (rootPath.isEmpty) return; - final dir = fs.directory(rootPath); - if (!await dir.exists()) return; - - await for (final entity in dir.list(recursive: true, followLinks: false)) { - if (entity is! Directory) continue; - final String basename = fs.path.basename(entity.path); - if (basename == 'App.framework.dSYM') { - final String machOPath = fs.path.join( - entity.path, - 'Contents', - 'Resources', - 'DWARF', - 'App', - ); - final File machOFile = fs.file(machOPath); - if (await machOFile.exists()) { - foundPaths.add(machOFile.absolute.path); - } - } - } - } - - // Search under the build directory directly to catch common iOS layouts - await collectAppleMachOUnder(config.buildFilesFolder); - - // Search all known roots (includes Fastlane ios/build) - await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { - await collectAppleMachOUnder(root); - } - - return foundPaths; -} - /// Enumerates the search roots used to discover native debug files, matching /// the existing behavior used by the plugin when uploading debug files. /// diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart index 3e0fbe96..c9b9c11e 100644 --- a/test/flutter_debug_files_test.dart +++ b/test/flutter_debug_files_test.dart @@ -1,7 +1,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; +import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; void main() { From e69117e13b5d25dd890cf561e85fcd4df94aaf92 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 14:58:58 +0200 Subject: [PATCH 09/88] Update --- lib/sentry_dart_plugin.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 21cd1de3..860edc1d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -102,8 +102,7 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - // Attempt (guarded) to upload Dart symbol map alongside relevant Flutter debug files. - await _tryUploadDartSymbolMapForDebugFiles(); + await _tryUploadDartSymbolMap(); Log.taskCompleted(taskName); } @@ -152,7 +151,7 @@ class SentryDartPlugin { /// Guarded implementation for uploading Dart symbol map alongside each relevant debug file. /// Currently a no-op until `_dartSymbolMapUploadEnabled` is flipped to true. - Future _tryUploadDartSymbolMapForDebugFiles() async { + Future _tryUploadDartSymbolMap() async { if (!_dartSymbolMapUploadEnabled) { Log.info('Dart symbol map upload is disabled in this version. Skipping.'); return; From 45f7bc216bd42eddc9d2c87bcb4bbc5a1b19953e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:02:18 +0200 Subject: [PATCH 10/88] Update --- lib/src/utils/dart_symbol_map.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index 6467562b..de21d2cb 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -32,8 +32,11 @@ Future isValidDartSymbolMapFile(File file) async { } } -/// If provided, validates and returns the absolute path to the Dart symbol map. -/// If not provided, returns null without scanning the filesystem. +/// If [configuredPath] is provided, validates and returns the absolute path to the Dart symbol map. +/// If not provided, returns null. +/// +/// Note: we do not scan the filesystem for this file because the file does not +/// have a special extension so worst case we would have to check every file. Future resolveDartSymbolMapPath({ required FileSystem fs, String? configuredPath, From e97c0dfc552a3dc1ddd58196f47f2a6c0cbe9bfb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:02:51 +0200 Subject: [PATCH 11/88] Update --- lib/src/utils/dart_symbol_map.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index de21d2cb..b8fa9ec9 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -11,12 +11,6 @@ import 'flutter_debug_files.dart'; /// (pairs of original and obfuscated names). Future isValidDartSymbolMapFile(File file) async { try { - // Basic size guard: reject extremely large files to avoid excessive memory usage. - final stat = await file.stat(); - // ~20MB upper bound is generous for typical obfuscation maps. - const int maxBytes = 20 * 1024 * 1024; - if (stat.size > maxBytes) return false; - final content = (await file.readAsString()).trim(); if (!(content.startsWith('[') && content.endsWith(']'))) return false; From 861540033fd6878d910fd5e77d782557d1b70947 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:03:23 +0200 Subject: [PATCH 12/88] Update --- lib/src/utils/dart_symbol_map.dart | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index b8fa9ec9..3e240f57 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -5,27 +5,6 @@ import 'package:file/file.dart'; import '../configuration.dart'; import 'flutter_debug_files.dart'; -/// Validates whether a file looks like a Dart obfuscation map. -/// -/// Expected minimal shape: top-level JSON array of strings with an even length -/// (pairs of original and obfuscated names). -Future isValidDartSymbolMapFile(File file) async { - try { - final content = (await file.readAsString()).trim(); - if (!(content.startsWith('[') && content.endsWith(']'))) return false; - - final decoded = jsonDecode(content); - if (decoded is! List) return false; - if (decoded.isEmpty || decoded.length.isOdd) return false; - for (final item in decoded) { - if (item is! String) return false; - } - return true; - } catch (_) { - return false; - } -} - /// If [configuredPath] is provided, validates and returns the absolute path to the Dart symbol map. /// If not provided, returns null. /// @@ -44,12 +23,6 @@ Future resolveDartSymbolMapPath({ ); } - if (!await isValidDartSymbolMapFile(file)) { - throw StateError( - "Invalid Dart symbol map at '$configuredPath'. It must be a JSON array of strings with an even number of elements.", - ); - } - return file.absolute.path; } From 8c97581cbd681a0e916ce54509c6963f549163d8 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:05:38 +0200 Subject: [PATCH 13/88] Update --- lib/src/utils/dart_symbol_map.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index 3e240f57..972b459e 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:file/file.dart'; import '../configuration.dart'; From 0502f233a90a454e411ced297ebc5d4fff15b2dd Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:06:39 +0200 Subject: [PATCH 14/88] Update --- lib/src/utils/dart_symbol_map.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index 972b459e..5407f5d4 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -61,17 +61,14 @@ Future> findFlutterRelevantDebugFilePaths({ } } - // First, scan the configured symbols folder (if any) if (config.symbolsFolder.isNotEmpty) { await collectAndroidSymbolsUnder(config.symbolsFolder); } - // Backward compatibility: also scan build folder if different if (config.buildFilesFolder != config.symbolsFolder) { await collectAndroidSymbolsUnder(config.buildFilesFolder); } - // Then, scan all current search roots used by the plugin await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { await collectAndroidSymbolsUnder(root); } @@ -100,10 +97,8 @@ Future> findFlutterRelevantDebugFilePaths({ } } - // Search under the build directory directly to catch common iOS layouts await collectAppleMachOUnder(config.buildFilesFolder); - // Search all known roots (includes Fastlane ios/build) await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { await collectAppleMachOUnder(root); } From 45247723ab4569bf5c6d467dbd7b1c560d87e2c1 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:07:14 +0200 Subject: [PATCH 15/88] Optimize --- lib/src/utils/dart_symbol_map.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index 5407f5d4..cb8ea3f8 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -69,10 +69,6 @@ Future> findFlutterRelevantDebugFilePaths({ await collectAndroidSymbolsUnder(config.buildFilesFolder); } - await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { - await collectAndroidSymbolsUnder(root); - } - Future collectAppleMachOUnder(String rootPath) async { if (rootPath.isEmpty) return; final dir = fs.directory(rootPath); @@ -101,6 +97,7 @@ Future> findFlutterRelevantDebugFilePaths({ await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { await collectAppleMachOUnder(root); + await collectAndroidSymbolsUnder(root); } return foundPaths; From 396507c71444826cd77cba4157139dd554e66c27 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:08:47 +0200 Subject: [PATCH 16/88] Update --- lib/sentry_dart_plugin.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index a60e1c4e..365a904d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -135,7 +135,7 @@ class SentryDartPlugin { // Guarded implementation for uploading Dart symbol map alongside each relevant debug file. // Currently no-ops until `_dartSymbolMapUploadEnabled` is flipped to true. -> Temporary guard. - Future _tryUploadDartSymbolMapForDebugFiles() async { + Future _tryUploadDartSymbolMap() async { if (!_dartSymbolMapUploadEnabled) { Log.info('Dart symbol map upload is disabled in this version. Skipping.'); return; @@ -147,9 +147,8 @@ class SentryDartPlugin { try { final fs = injector.get(); - // Check for explicitly provided and valid Dart symbol map. - final symbolMapPath = - await findDartSymbolMapPath(fs: fs, config: _configuration); + final symbolMapPath = await resolveDartSymbolMapPath( + fs: fs, configuredPath: _configuration.dartSymbolMapPath); if (symbolMapPath == null) { Log.warn( "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Skipping Dart symbol map uploads."); @@ -170,7 +169,6 @@ class SentryDartPlugin { } for (final debugFilePath in debugFilePaths) { - // Accept both files and directories, mirroring current symbol search behavior. final isFile = await fs.file(debugFilePath).exists(); final isDir = await fs.directory(debugFilePath).exists(); if (!isFile && !isDir) { @@ -191,11 +189,10 @@ class SentryDartPlugin { params, ); } - - Log.taskCompleted(taskName); - } on Exception catch (e) { + } catch (e) { Log.error('Dart symbol map upload failed: $e'); - throw ExitError(1); + } finally { + Log.taskCompleted(taskName); } } From 1eea0d84c05821fba3c46c7e8cd664c09b8d0a26 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:09:38 +0200 Subject: [PATCH 17/88] Update --- lib/sentry_dart_plugin.dart | 63 ------------------------------------- 1 file changed, 63 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 365a904d..860edc1d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -133,69 +133,6 @@ class SentryDartPlugin { return result; } - // Guarded implementation for uploading Dart symbol map alongside each relevant debug file. - // Currently no-ops until `_dartSymbolMapUploadEnabled` is flipped to true. -> Temporary guard. - Future _tryUploadDartSymbolMap() async { - if (!_dartSymbolMapUploadEnabled) { - Log.info('Dart symbol map upload is disabled in this version. Skipping.'); - return; - } - - const taskName = 'uploading Dart symbol map(s)'; - Log.startingTask(taskName); - - try { - final fs = injector.get(); - - final symbolMapPath = await resolveDartSymbolMapPath( - fs: fs, configuredPath: _configuration.dartSymbolMapPath); - if (symbolMapPath == null) { - Log.warn( - "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Skipping Dart symbol map uploads."); - Log.taskCompleted(taskName); - return; - } - - final debugFilePaths = await findFlutterRelevantDebugFilePaths( - fs: fs, - config: _configuration, - ); - - if (debugFilePaths.isEmpty) { - Log.info( - 'No Flutter-relevant debug files found for Dart symbol map upload.'); - Log.taskCompleted(taskName); - return; - } - - for (final debugFilePath in debugFilePaths) { - final isFile = await fs.file(debugFilePath).exists(); - final isDir = await fs.directory(debugFilePath).exists(); - if (!isFile && !isDir) { - continue; - } - - final params = []; - _setUrlAndTokenAndLog(params); - params.add('dart-symbol-map'); - params.add('upload'); - _addOrgAndProject(params); - _addWait(params); - params.add(symbolMapPath); - params.add(debugFilePath); - - await _executeAndLog( - 'Failed to upload Dart symbol map for $debugFilePath', - params, - ); - } - } catch (e) { - Log.error('Dart symbol map upload failed: $e'); - } finally { - Log.taskCompleted(taskName); - } - } - List _baseCliParams({bool addReleases = false}) { final params = []; if (addReleases) { From c420e726763b916bf7b534f673a5c98f3bbd6b19 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 15:29:17 +0200 Subject: [PATCH 18/88] Remove unnecessary file check --- lib/src/utils/dart_symbol_map.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index cb8ea3f8..5f05fb48 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -49,16 +49,6 @@ Future> findFlutterRelevantDebugFilePaths({ } return; } - - final file = fs.file(rootPath); - if (await file.exists()) { - final String basename = fs.path.basename(file.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin')) { - foundPaths.add(file.absolute.path); - } - } } if (config.symbolsFolder.isNotEmpty) { From 0cd79e72a2d7bb6cbe0b5118cbe0cb6744880087 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:01:51 +0200 Subject: [PATCH 19/88] Review --- lib/sentry_dart_plugin.dart | 26 ++++++++++++++------------ lib/src/utils/dart_symbol_map.dart | 21 --------------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 860edc1d..d473d040 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -161,16 +161,20 @@ class SentryDartPlugin { Log.startingTask(taskName); try { - final fs = injector.get(); + if (_configuration.dartSymbolMapPath == null || + _configuration.dartSymbolMapPath!.isEmpty) { + Log.warn( + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + ); + Log.taskCompleted(taskName); + return; + } - // Check for explicitly provided and valid Dart symbol map (wire to config in a follow-up). - final symbolMapPath = await resolveDartSymbolMapPath( - fs: fs, - configuredPath: null, - ); - if (symbolMapPath == null) { + final fs = injector.get(); + final symbolMapPath = _configuration.dartSymbolMapPath!; + if (!await fs.file(symbolMapPath).exists()) { Log.warn( - "No 'dart_symbol_map_path' provided. Set --sentry-define --dart_symbol_map_path=/abs/path/to/map to enable Dart obfuscation map usage. Skipping Dart symbol map uploads.", + "Skipping Dart symbol map uploads: Dart symbol map file not found at '$_configuration.dartSymbolMapPath'.", ); Log.taskCompleted(taskName); return; @@ -183,13 +187,12 @@ class SentryDartPlugin { if (debugFilePaths.isEmpty) { Log.info( - 'No Flutter-relevant debug files found for Dart symbol map upload.'); + 'Skipping Dart symbol map uploads: no Flutter-relevant debug files found.'); Log.taskCompleted(taskName); return; } for (final debugFilePath in debugFilePaths) { - // Accept both files and directories, mirroring current symbol search behavior. final isFile = await fs.file(debugFilePath).exists(); final isDir = await fs.directory(debugFilePath).exists(); if (!isFile && !isDir) { @@ -210,10 +213,9 @@ class SentryDartPlugin { params, ); } - - Log.taskCompleted(taskName); } catch (e) { Log.error('Dart symbol map upload failed: $e'); + } finally { Log.taskCompleted(taskName); } } diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart index 5f05fb48..cd0e7b0b 100644 --- a/lib/src/utils/dart_symbol_map.dart +++ b/lib/src/utils/dart_symbol_map.dart @@ -3,27 +3,6 @@ import 'package:file/file.dart'; import '../configuration.dart'; import 'flutter_debug_files.dart'; -/// If [configuredPath] is provided, validates and returns the absolute path to the Dart symbol map. -/// If not provided, returns null. -/// -/// Note: we do not scan the filesystem for this file because the file does not -/// have a special extension so worst case we would have to check every file. -Future resolveDartSymbolMapPath({ - required FileSystem fs, - String? configuredPath, -}) async { - if (configuredPath == null || configuredPath.isEmpty) return null; - - final file = fs.file(configuredPath); - if (!await file.exists()) { - throw StateError( - "Dart symbol map file not found at '$configuredPath'. Ensure the path is correct and the file exists.", - ); - } - - return file.absolute.path; -} - /// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS) /// that should be paired with a Dart symbol map. Future> findFlutterRelevantDebugFilePaths({ From a49d2de0a7636ae68ea12b131646137e52d6a855 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:05:31 +0200 Subject: [PATCH 20/88] Add missing test --- test/configuration_test.dart | 10 ++++++++++ test/configuration_values_test.dart | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 10fe2df0..a1bb1398 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -25,16 +25,19 @@ void main() { release: 'release-platformEnv-config', dist: 'dist-platformEnv-config', sentryCliCdnUrl: 'sentryCliCdnUrl-platformEnv-config', + dartSymbolMapPath: 'dart-symbol-map-platformEnv-config', ); final argsConfig = ConfigurationValues( release: 'release-args-config', dist: 'dist-args-config', sentryCliCdnUrl: 'sentryCliCdnUrl-args-config', + dartSymbolMapPath: 'dart-symbol-map-args-config', ); final fileConfig = ConfigurationValues( release: 'release-file-config', dist: 'dist-file-config', sentryCliCdnUrl: 'sentryCliCdnUrl-file-config', + dartSymbolMapPath: 'dart-symbol-map-file-config', ); final sut = fixture.getSut( @@ -46,6 +49,7 @@ void main() { expect(sut.release, 'release-platformEnv-config'); expect(sut.dist, 'dist-platformEnv-config'); expect(sut.sentryCliCdnUrl, 'sentryCliCdnUrl-platformEnv-config'); + expect(sut.dartSymbolMapPath, 'dart-symbol-map-platformEnv-config'); }); // env config @@ -62,6 +66,7 @@ void main() { org: 'org-args-config', authToken: 'auth_token-args-config', url: 'url-args-config', + dartSymbolMapPath: 'args-dart-symbol-map.json', urlPrefix: 'url-prefix-args-config', waitForProcessing: true, logLevel: 'warning', @@ -88,6 +93,7 @@ void main() { org: 'org-file-config', authToken: 'auth_token-file-config', url: 'url-file-config', + dartSymbolMapPath: 'file-dart-symbol-map.json', urlPrefix: 'url-prefix-file-config', waitForProcessing: false, logLevel: 'debug', @@ -120,6 +126,7 @@ void main() { expect(sut.org, 'org-args-config'); expect(sut.authToken, 'auth_token-args-config'); expect(sut.url, 'url-args-config'); + expect(sut.dartSymbolMapPath, 'args-dart-symbol-map.json'); expect(sut.urlPrefix, 'url-prefix-args-config'); expect(sut.waitForProcessing, isTrue); expect(sut.logLevel, 'warning'); @@ -156,6 +163,7 @@ void main() { org: 'org-file-config', authToken: 'auth_token-file-config', url: 'url-file-config', + dartSymbolMapPath: 'file-dart-symbol-map.json', urlPrefix: 'url-prefix-file-config', waitForProcessing: true, logLevel: 'debug', @@ -189,6 +197,7 @@ void main() { expect(sut.org, 'org-file-config'); expect(sut.authToken, 'auth_token-file-config'); expect(sut.url, 'url-file-config'); + expect(sut.dartSymbolMapPath, 'file-dart-symbol-map.json'); expect(sut.urlPrefix, 'url-prefix-file-config'); expect(sut.waitForProcessing, isTrue); expect(sut.logLevel, 'debug'); @@ -226,6 +235,7 @@ void main() { expect(sut.uploadDebugSymbols, isTrue); expect(sut.uploadSourceMaps, isFalse); expect(sut.uploadSources, isFalse); + expect(sut.dartSymbolMapPath, isNull); expect(sut.commits, 'auto'); expect(sut.ignoreMissing, isFalse); expect(sut.buildFilesFolder, 'build'); diff --git a/test/configuration_values_test.dart b/test/configuration_values_test.dart index 59725141..d5432b03 100644 --- a/test/configuration_values_test.dart +++ b/test/configuration_values_test.dart @@ -23,6 +23,7 @@ void main() { "--sentry-define=org=fixture-org", "--sentry-define=auth_token=fixture-auth_token", "--sentry-define=url=fixture-url", + "--sentry-define=dart_symbol_map_path=fixture-dart-symbol-map.json", "--sentry-define=wait_for_processing=true", "--sentry-define=log_level=fixture-log_level", "--sentry-define=release=fixture-release", @@ -47,6 +48,7 @@ void main() { expect(sut.org, 'fixture-org'); expect(sut.authToken, 'fixture-auth_token'); expect(sut.url, 'fixture-url'); + expect(sut.dartSymbolMapPath, 'fixture-dart-symbol-map.json'); expect(sut.waitForProcessing, isTrue); expect(sut.logLevel, 'fixture-log_level'); expect(sut.release, 'fixture-release'); @@ -90,6 +92,7 @@ void main() { upload_source_maps: true upload_sources: true url: fixture-url + dart_symbol_map_path: fixture-dart-symbol-map.json wait_for_processing: true log_level: fixture-log_level release: fixture-release @@ -133,6 +136,7 @@ void main() { expect(sut.org, 'o'); expect(sut.authToken, 't'); expect(sut.url, 'fixture-url'); + expect(sut.dartSymbolMapPath, 'fixture-dart-symbol-map.json'); expect(sut.waitForProcessing, isTrue); expect(sut.logLevel, 'fixture-log_level'); expect(sut.release, 'fixture-release'); @@ -155,6 +159,7 @@ void main() { upload_source_maps=true upload_sources=true url=fixture-url + dart_symbol_map_path=fixture-dart-symbol-map.json wait_for_processing=true log_level=fixture-log_level release=fixture-release @@ -197,6 +202,7 @@ void main() { expect(sut.org, 'o'); expect(sut.authToken, 't'); expect(sut.url, 'fixture-url'); + expect(sut.dartSymbolMapPath, 'fixture-dart-symbol-map.json'); expect(sut.waitForProcessing, isTrue); expect(sut.logLevel, 'fixture-log_level'); expect(sut.release, 'fixture-release'); @@ -275,12 +281,14 @@ void main() { 'SENTRY_RELEASE': 'fixture-release', 'SENTRY_DIST': 'fixture-dist', 'SENTRYCLI_CDNURL': 'fixture-sentry_cli_cdn_url', + 'SENTRY_DART_SYMBOL_MAP_PATH': 'fixture-env-dart-symbol-map.json', }; final sut = ConfigurationValues.fromPlatformEnvironment(arguments); expect(sut.release, 'fixture-release'); expect(sut.dist, 'fixture-dist'); expect(sut.sentryCliCdnUrl, 'fixture-sentry_cli_cdn_url'); + expect(sut.dartSymbolMapPath, 'fixture-env-dart-symbol-map.json'); }); }); } From 82f1afcb595e691c01e020c004ace6426c193d37 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:07:50 +0200 Subject: [PATCH 21/88] Update tests --- test/dart_symbol_map_integration_test.dart | 62 -------------------- test/dart_symbol_map_test.dart | 67 ---------------------- 2 files changed, 129 deletions(-) delete mode 100644 test/dart_symbol_map_integration_test.dart delete mode 100644 test/dart_symbol_map_test.dart diff --git a/test/dart_symbol_map_integration_test.dart b/test/dart_symbol_map_integration_test.dart deleted file mode 100644 index 84488f64..00000000 --- a/test/dart_symbol_map_integration_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; -import 'package:test/test.dart'; - -void main() { - late FileSystem fs; - late Configuration config; - - setUp(() { - fs = MemoryFileSystem.test(); - config = Configuration(); - // Ensure predictable build folder default for tests, though scanning is disabled. - config.buildFilesFolder = 'build'; - config.dartSymbolMapPath = null; - }); - - group('findDartSymbolMapPath (explicit path only, no scanning)', () { - test('returns absolute path when explicit path is valid', () async { - final file = fs.file('map.json') - ..createSync(recursive: true) - ..writeAsStringSync('["A","B"]'); - - config.dartSymbolMapPath = 'map.json'; - - final result = await findDartSymbolMapPath(fs: fs, config: config); - expect(result, equals(file.absolute.path)); - }); - - test('throws when explicit path is missing', () async { - config.dartSymbolMapPath = 'missing.json'; - expect( - () => findDartSymbolMapPath(fs: fs, config: config), - throwsA(isA()), - ); - }); - - test('throws when explicit path exists but is invalid', () async { - final file = fs.file('invalid.json') - ..createSync(recursive: true) - ..writeAsStringSync('[]'); // empty array is invalid - config.dartSymbolMapPath = file.path; - expect( - () => findDartSymbolMapPath(fs: fs, config: config), - throwsA(isA()), - ); - }); - - test('returns null when explicit path is not provided', () async { - // Place a valid map file somewhere under build to verify no scanning. - fs.directory('build/assets').createSync(recursive: true); - fs.file('build/assets/some_map') - ..createSync() - ..writeAsStringSync('["A","B"]'); - - config.dartSymbolMapPath = null; - final result = await findDartSymbolMapPath(fs: fs, config: config); - expect(result, isNull); - }); - }); -} diff --git a/test/dart_symbol_map_test.dart b/test/dart_symbol_map_test.dart deleted file mode 100644 index 511563e8..00000000 --- a/test/dart_symbol_map_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:typed_data'; - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; -import 'package:test/test.dart'; - -void main() { - late FileSystem fs; - - setUp(() { - fs = MemoryFileSystem.test(); - }); - - group('isValidDartSymbolMapFile', () { - test('valid when array of strings with even length (single pair)', - () async { - final file = fs.file('map.json')..writeAsStringSync('["A","B"]'); - expect(await isValidDartSymbolMapFile(file), isTrue); - }); - - test('valid when array has multiple pairs', () async { - final file = fs.file('map_multi.json') - ..writeAsStringSync('["MaterialApp","ex","Scaffold","ey"]'); - expect(await isValidDartSymbolMapFile(file), isTrue); - }); - - test('valid when trailing whitespace/newline present', () async { - final file = fs.file('map_ws.json')..writeAsStringSync('["A","B"]\n '); - expect(await isValidDartSymbolMapFile(file), isTrue); - }); - - test('invalid when empty array', () async { - final file = fs.file('empty.json')..writeAsStringSync('[]'); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - - test('invalid when odd number of elements', () async { - final file = fs.file('odd.json')..writeAsStringSync('["A"]'); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - - test('invalid when non-string element present', () async { - final file = fs.file('non_string.json')..writeAsStringSync('[1, "A"]'); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - - test('invalid when top-level JSON is not an array', () async { - final file = fs.file('not_array.json')..writeAsStringSync('{"a":"b"}'); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - - test('invalid when malformed JSON', () async { - final file = fs.file('malformed.json')..writeAsStringSync('['); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - - test('invalid when file larger than maximum threshold', () async { - final file = fs.file('too_big'); - // Create a file of size kMaxDartSymbolMapSizeBytes + 1 without valid JSON - // to trigger size-based rejection without decoding. - final tooBig = kMaxDartSymbolMapSizeBytes + 1; - await file.writeAsBytes(Uint8List(tooBig)); - expect(await isValidDartSymbolMapFile(file), isFalse); - }); - }); -} From 035af1b02e935f6b268c172830d3f28065b54955 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:22:50 +0200 Subject: [PATCH 22/88] Update tests --- test/plugin_test.dart | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 3dc8a715..1aee1855 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -217,6 +217,50 @@ void main() { }); }); + test('emits dart-symbol-map upload command when enabled', () async { + const version = '1.0.0'; + // Create Android symbols and a fake Dart symbol map + final androidSymbolsDir = fs.directory('$buildDir/app/outputs') + ..createSync(recursive: true); + fs.file('${androidSymbolsDir.path}/app-release.symbols') + ..writeAsStringSync('fake'); + final mapFile = fs.file('obfuscation.map.json') + ..writeAsStringSync('[]'); + + final config = ''' + upload_debug_symbols: true + log_level: debug + dart_symbol_map_path: ${mapFile.path} + '''; + + final commandLog = await runWith(version, config); + const release = '$name@$version'; + + final args = '$commonArgs --log-level debug'; + // Accept either relative or absolute path and potential '/./' normalization in the debug file path + expect( + commandLog, + anyElement((e) => + e.startsWith( + '$cli $args dart-symbol-map upload $orgAndProject ${mapFile.path} ') && + e.endsWith('$buildDir/app/outputs/app-release.symbols')), + ); + + // Ensure other expected commands still present + expect( + commandLog, + contains( + '$cli $args debug-files upload $orgAndProject $buildDir/app/outputs')); + expect(commandLog, + contains('$cli $args releases $orgAndProject new $release')); + expect( + commandLog, + contains( + '$cli $args releases $orgAndProject set-commits $release --auto')); + expect(commandLog, + contains('$cli $args releases $orgAndProject finalize $release')); + }); + group('release', () { test('default from name and version', () async { const version = '1.0.0'; From b4d9a97357ff0127603c6f8d75f9a8962d541a89 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:37:01 +0200 Subject: [PATCH 23/88] Update tests --- test/plugin_test.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 1aee1855..2944a21d 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -222,8 +222,9 @@ void main() { // Create Android symbols and a fake Dart symbol map final androidSymbolsDir = fs.directory('$buildDir/app/outputs') ..createSync(recursive: true); - fs.file('${androidSymbolsDir.path}/app-release.symbols') - ..writeAsStringSync('fake'); + fs + .file('${androidSymbolsDir.path}/app-release.symbols') + .writeAsStringSync('fake'); final mapFile = fs.file('obfuscation.map.json') ..writeAsStringSync('[]'); From 832e5b82f25293a0afb53d3bbfacac375b9647fb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:34:11 +0200 Subject: [PATCH 24/88] Update --- .../dart_map_debug_file_collector.dart | 87 ++++++++++ test/dart_map_debug_files_collector_test.dart | 159 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 lib/src/symbol_maps/dart_map_debug_file_collector.dart create mode 100644 test/dart_map_debug_files_collector_test.dart diff --git a/lib/src/symbol_maps/dart_map_debug_file_collector.dart b/lib/src/symbol_maps/dart_map_debug_file_collector.dart new file mode 100644 index 00000000..96ee94a1 --- /dev/null +++ b/lib/src/symbol_maps/dart_map_debug_file_collector.dart @@ -0,0 +1,87 @@ +import 'package:file/file.dart'; + +import '../configuration.dart'; +import '../utils/flutter_debug_files.dart'; + +/// Collects Flutter-relevant native debug file paths that should be paired +/// with a Dart symbol map for symbolication. +/// +/// Policy: +/// - Android: include Flutter-generated `.symbols` files (e.g., +/// `app.android-arm.symbols`, `app.android-arm64.symbols`, `app.android-x64.symbols`). +/// - Apple: include the Mach-O binary `App` inside +/// `App.framework.dSYM/Contents/Resources/DWARF/App`. +/// +/// The function returns absolute, deduplicated paths. It enumerates the +/// configured `symbolsFolder`, `buildFilesFolder`, and other Flutter +/// search roots discovered by `enumerateDebugSearchRoots`. +Future> collectDebugFilesForDartMap({ + required FileSystem fs, + required Configuration config, +}) async { + final Set foundPaths = {}; + + Future collectAndroidSymbolsUnder(String rootPath) async { + if (rootPath.isEmpty) return; + + final Directory directory = fs.directory(rootPath); + if (!await directory.exists()) return; + + await for (final FileSystemEntity entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(fs.file(entity.path).absolute.path); + } + } + } + + Future collectAppleMachOUnder(String rootPath) async { + if (rootPath.isEmpty) return; + final Directory dir = fs.directory(rootPath); + if (!await dir.exists()) return; + + await for (final FileSystemEntity entity + in dir.list(recursive: true, followLinks: false)) { + if (entity is! Directory) continue; + final String basename = fs.path.basename(entity.path); + if (basename == 'App.framework.dSYM') { + final String machOPath = fs.path.join( + entity.path, + 'Contents' + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } + } + } + + // Prefer explicit folders first. + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); + } + + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); + } + + // Collect Apple Mach-O directly under the configured build files folder. + await collectAppleMachOUnder(config.buildFilesFolder); + + // Enumerate additional Flutter-related roots. + await for (final String root + in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAppleMachOUnder(root); + await collectAndroidSymbolsUnder(root); + } + + return foundPaths; +} diff --git a/test/dart_map_debug_files_collector_test.dart b/test/dart_map_debug_files_collector_test.dart new file mode 100644 index 00000000..e3e1c4d6 --- /dev/null +++ b/test/dart_map_debug_files_collector_test.dart @@ -0,0 +1,159 @@ +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_debug_file_collector.dart'; +import 'package:sentry_dart_plugin/src/configuration.dart'; + +void main() { + group('collectDebugFilesForDartMap', () { + test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', + () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/work')..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/work/build'; + final symbolsDir = '/work/symbols'; + + // Android .symbols files + fs + .file('$symbolsDir/app.android-arm.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-arm64.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-x64.symbols') + .createSync(recursive: true); + + // Apple App.framework.dSYM Mach-O + final appDsymMachO = + '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(appDsymMachO).createSync(recursive: true); + + // Noise: other .dSYM bundles should be ignored + fs + .file( + '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect( + result, + containsAll([ + fs.path.normalize('/work/symbols/app.android-arm.symbols'), + fs.path.normalize('/work/symbols/app.android-arm64.symbols'), + fs.path.normalize('/work/symbols/app.android-x64.symbols'), + fs.path.normalize(appDsymMachO), + ])); + + // Ensure we did not include non-App.framework dSYMs + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + + // Ensure deduplication and absoluteness + expect(result.length, 4); + for (final p in result) { + expect(p.startsWith('/'), isTrue, + reason: 'path should be absolute: $p'); + } + }); + + test('finds App.framework.dSYM under Fastlane ios/build path', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/project') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/project/build'; + final symbolsDir = '/project/symbols'; + + // Fastlane path + final machO = + '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(machO).createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(machO))); + }); + + test('finds App.framework.dSYM in macOS build products', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/macosproj') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/macosproj/build'; + final symbolsDir = '/macosproj/symbols'; + + // macOS Products Release path + final macMachO = + '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(macMachO).createSync(recursive: true); + + // Noise: other dSYMs should be ignored + fs + .file( + '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(macMachO))); + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + }); + + test('returns empty set when no roots or symbols exist', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/empty') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/empty/build'; + final symbolsDir = '/empty/symbols'; + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, isEmpty); + }); + }); +} From 4da74326b9c41288a94b1649b5446a6aea334bf9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 20:12:28 +0200 Subject: [PATCH 25/88] Update --- lib/sentry_dart_plugin.dart | 4 +- .../dart_map_debug_file_collector.dart | 4 +- lib/src/symbol_maps/dart_map_discovery.dart | 31 ++++++++ lib/src/utils/dart_symbol_map.dart | 73 ------------------- test/dart_map_discovery_test.dart | 63 ++++++++++++++++ test/flutter_debug_files_test.dart | 12 +-- 6 files changed, 104 insertions(+), 83 deletions(-) create mode 100644 lib/src/symbol_maps/dart_map_discovery.dart delete mode 100644 lib/src/utils/dart_symbol_map.dart create mode 100644 test/dart_map_discovery_test.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index d473d040..5d9c343d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -6,7 +6,7 @@ import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; -import 'src/utils/dart_symbol_map.dart'; +import 'src/symbol_maps/dart_map_debug_file_collector.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -180,7 +180,7 @@ class SentryDartPlugin { return; } - final debugFilePaths = await findFlutterRelevantDebugFilePaths( + final debugFilePaths = await collectDebugFilesForDartMap( fs: fs, config: _configuration, ); diff --git a/lib/src/symbol_maps/dart_map_debug_file_collector.dart b/lib/src/symbol_maps/dart_map_debug_file_collector.dart index 96ee94a1..7c22439e 100644 --- a/lib/src/symbol_maps/dart_map_debug_file_collector.dart +++ b/lib/src/symbol_maps/dart_map_debug_file_collector.dart @@ -51,8 +51,8 @@ Future> collectDebugFilesForDartMap({ if (basename == 'App.framework.dSYM') { final String machOPath = fs.path.join( entity.path, - 'Contents' - 'Resources', + 'Contents', + 'Resources', 'DWARF', 'App', ); diff --git a/lib/src/symbol_maps/dart_map_discovery.dart b/lib/src/symbol_maps/dart_map_discovery.dart new file mode 100644 index 00000000..fabe94e2 --- /dev/null +++ b/lib/src/symbol_maps/dart_map_discovery.dart @@ -0,0 +1,31 @@ +import 'package:file/file.dart'; + +import '../configuration.dart'; +import '../utils/log.dart'; + +/// Resolves an absolute path to the Dart obfuscation map file if provided. +/// +/// Behavior: +/// - When `config.dartSymbolMapPath` is null or empty, logs a warning and returns null. +/// - When the provided path does not exist, logs a warning and returns null. +/// - Otherwise, returns the absolute path to the map file. +Future resolveDartMapPath({ + required FileSystem fs, + required Configuration config, +}) async { + final String? providedPath = config.dartSymbolMapPath?.trim(); + if (providedPath == null || providedPath.isEmpty) { + Log.warn( + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided."); + return null; + } + + final File file = fs.file(providedPath); + if (!await file.exists()) { + Log.warn( + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'."); + return null; + } + + return file.absolute.path; +} diff --git a/lib/src/utils/dart_symbol_map.dart b/lib/src/utils/dart_symbol_map.dart deleted file mode 100644 index cd0e7b0b..00000000 --- a/lib/src/utils/dart_symbol_map.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:file/file.dart'; - -import '../configuration.dart'; -import 'flutter_debug_files.dart'; - -/// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS) -/// that should be paired with a Dart symbol map. -Future> findFlutterRelevantDebugFilePaths({ - required FileSystem fs, - required Configuration config, -}) async { - final Set foundPaths = {}; - - Future collectAndroidSymbolsUnder(String rootPath) async { - if (rootPath.isEmpty) return; - - final directory = fs.directory(rootPath); - if (await directory.exists()) { - await for (final entity - in directory.list(recursive: true, followLinks: false)) { - if (entity is! File) continue; - final String basename = fs.path.basename(entity.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin')) { - foundPaths.add(fs.file(entity.path).absolute.path); - } - } - return; - } - } - - if (config.symbolsFolder.isNotEmpty) { - await collectAndroidSymbolsUnder(config.symbolsFolder); - } - - if (config.buildFilesFolder != config.symbolsFolder) { - await collectAndroidSymbolsUnder(config.buildFilesFolder); - } - - Future collectAppleMachOUnder(String rootPath) async { - if (rootPath.isEmpty) return; - final dir = fs.directory(rootPath); - if (!await dir.exists()) return; - - await for (final entity in dir.list(recursive: true, followLinks: false)) { - if (entity is! Directory) continue; - final String basename = fs.path.basename(entity.path); - if (basename == 'App.framework.dSYM') { - final String machOPath = fs.path.join( - entity.path, - 'Contents', - 'Resources', - 'DWARF', - 'App', - ); - final File machOFile = fs.file(machOPath); - if (await machOFile.exists()) { - foundPaths.add(machOFile.absolute.path); - } - } - } - } - - await collectAppleMachOUnder(config.buildFilesFolder); - - await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { - await collectAppleMachOUnder(root); - await collectAndroidSymbolsUnder(root); - } - - return foundPaths; -} diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart new file mode 100644 index 00000000..332d77f5 --- /dev/null +++ b/test/dart_map_discovery_test.dart @@ -0,0 +1,63 @@ +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_discovery.dart'; + +void main() { + group('resolveDartMapPath', () { + test('returns absolute path for absolute input', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRoot = fs.directory('/proj')..createSync(recursive: true); + fs.currentDirectory = projectRoot; + + final absolutePath = '/proj/maps/obfuscation.json'; + fs.file(absolutePath).createSync(recursive: true); + + final config = Configuration()..dartSymbolMapPath = absolutePath; + + final result = await resolveDartMapPath(fs: fs, config: config); + expect(result, equals(absolutePath)); + }); + + test('resolves relative path to absolute when file exists', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRoot = fs.directory('/root')..createSync(recursive: true); + fs.currentDirectory = projectRoot; + + final rel = 'build/app/obfuscation.map'; + final abs = '/root/build/app/obfuscation.map'; + fs.file(abs).createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = '/root/build' + ..symbolsFolder = '/root/symbols' + ..dartSymbolMapPath = rel; + + final result = await resolveDartMapPath(fs: fs, config: config); + expect(result, equals(abs)); + }); + + test('returns null and warns when path not provided', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRoot = fs.directory('/x')..createSync(recursive: true); + fs.currentDirectory = projectRoot; + + final config = Configuration()..dartSymbolMapPath = null; + + final result = await resolveDartMapPath(fs: fs, config: config); + expect(result, isNull); + }); + + test('returns null and warns when file does not exist', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRoot = fs.directory('/p')..createSync(recursive: true); + fs.currentDirectory = projectRoot; + + final config = Configuration()..dartSymbolMapPath = 'missing.map'; + + final result = await resolveDartMapPath(fs: fs, config: config); + expect(result, isNull); + }); + }); +} diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart index c9b9c11e..e3e1c4d6 100644 --- a/test/flutter_debug_files_test.dart +++ b/test/flutter_debug_files_test.dart @@ -1,11 +1,11 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/utils/dart_symbol_map.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_debug_file_collector.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; void main() { - group('findFlutterRelevantDebugFilePaths', () { + group('collectDebugFilesForDartMap', () { test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', () async { final fs = MemoryFileSystem(style: FileSystemStyle.posix); @@ -45,7 +45,7 @@ void main() { ..buildFilesFolder = buildDir ..symbolsFolder = symbolsDir; - final result = await findFlutterRelevantDebugFilePaths( + final result = await collectDebugFilesForDartMap( fs: fs, config: config, ); @@ -89,7 +89,7 @@ void main() { ..buildFilesFolder = buildDir ..symbolsFolder = symbolsDir; - final result = await findFlutterRelevantDebugFilePaths( + final result = await collectDebugFilesForDartMap( fs: fs, config: config, ); @@ -125,7 +125,7 @@ void main() { ..buildFilesFolder = buildDir ..symbolsFolder = symbolsDir; - final result = await findFlutterRelevantDebugFilePaths( + final result = await collectDebugFilesForDartMap( fs: fs, config: config, ); @@ -148,7 +148,7 @@ void main() { ..buildFilesFolder = buildDir ..symbolsFolder = symbolsDir; - final result = await findFlutterRelevantDebugFilePaths( + final result = await collectDebugFilesForDartMap( fs: fs, config: config, ); From 5eff2dbe48218840c3a07bec06553471eeacedee Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 20:37:41 +0200 Subject: [PATCH 26/88] Update --- lib/sentry_dart_plugin.dart | 49 ++---- lib/src/symbol_maps/dart_map_uploader.dart | 167 ++++++++++++++++++++ test/dart_map_uploader_test.dart | 173 +++++++++++++++++++++ 3 files changed, 354 insertions(+), 35 deletions(-) create mode 100644 lib/src/symbol_maps/dart_map_uploader.dart create mode 100644 test/dart_map_uploader_test.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 5d9c343d..57907e9c 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -7,6 +7,8 @@ import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; import 'src/symbol_maps/dart_map_debug_file_collector.dart'; +import 'src/symbol_maps/dart_map_discovery.dart'; +import 'src/symbol_maps/dart_map_uploader.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -161,26 +163,16 @@ class SentryDartPlugin { Log.startingTask(taskName); try { - if (_configuration.dartSymbolMapPath == null || - _configuration.dartSymbolMapPath!.isEmpty) { - Log.warn( - "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - ); - Log.taskCompleted(taskName); - return; - } - final fs = injector.get(); - final symbolMapPath = _configuration.dartSymbolMapPath!; - if (!await fs.file(symbolMapPath).exists()) { - Log.warn( - "Skipping Dart symbol map uploads: Dart symbol map file not found at '$_configuration.dartSymbolMapPath'.", - ); + + final String? resolvedMapPath = + await resolveDartMapPath(fs: fs, config: _configuration); + if (resolvedMapPath == null) { Log.taskCompleted(taskName); return; } - final debugFilePaths = await collectDebugFilesForDartMap( + final Set debugFilePaths = await collectDebugFilesForDartMap( fs: fs, config: _configuration, ); @@ -192,27 +184,14 @@ class SentryDartPlugin { return; } - for (final debugFilePath in debugFilePaths) { - final isFile = await fs.file(debugFilePath).exists(); - final isDir = await fs.directory(debugFilePath).exists(); - if (!isFile && !isDir) { - continue; - } + Log.info("Resolved Dart symbol map at '$resolvedMapPath'"); + Log.info('Found ${debugFilePaths.length} debug file(s) to pair with.'); - final params = []; - _setUrlAndTokenAndLog(params); - params.add('dart-symbol-map'); - params.add('upload'); - _addOrgAndProject(params); - _addWait(params); - params.add(symbolMapPath); - params.add(debugFilePath); - - await _executeAndLog( - 'Failed to upload Dart symbol map for $debugFilePath', - params, - ); - } + await DartMapUploader.upload( + config: _configuration, + symbolMapPath: resolvedMapPath, + debugFilePaths: debugFilePaths, + ); } catch (e) { Log.error('Dart symbol map upload failed: $e'); } finally { diff --git a/lib/src/symbol_maps/dart_map_uploader.dart b/lib/src/symbol_maps/dart_map_uploader.dart new file mode 100644 index 00000000..1c454c30 --- /dev/null +++ b/lib/src/symbol_maps/dart_map_uploader.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:process/process.dart'; + +import '../configuration.dart'; +import '../utils/injector.dart'; +import '../utils/log.dart'; + +/// Uploads a Dart obfuscation map paired with each provided native debug file. +/// +/// For every [debugFilePaths] entry, this emits one CLI invocation equivalent to: +/// +/// sentry-cli dart-symbol-map upload [--url ...] [--auth-token ...] +/// [--log-level ...] --org ... --project ... [--wait] +/// +/// +/// Stdout/stderr from the underlying process are forwarded to [Log]. A +/// non-zero exit code triggers [ExitError] (via [Log.processExitCode]). +class DartMapUploader { + /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. + /// + /// Throws [ExitError] on the first non-zero CLI exit code. + static Future upload({ + required Configuration config, + required String symbolMapPath, + required Iterable debugFilePaths, + }) async { + final ProcessManager processManager = injector.get(); + + int attempted = 0; + int succeeded = 0; + int failed = 0; + + try { + for (final String debugFilePath in debugFilePaths) { + attempted++; + + Log.info( + "Uploading Dart symbol map '$symbolMapPath' paired with '$debugFilePath'"); + + final List params = _buildBaseParams(config) + ..addAll(['dart-symbol-map', 'upload']) + ..addAll(_orgAndProjectParams(config)) + ..addAll(_waitParam(config)) + ..add(symbolMapPath) + ..add(debugFilePath); + + final int exitCode = await _startAndForward( + processManager: processManager, + cliPath: config.cliPath!, + params: params, + errorContext: 'Failed to upload Dart symbol map for $debugFilePath', + ); + + if (exitCode == 0) { + succeeded++; + } else { + failed++; + } + + // Propagate non-zero exit code consistently with the plugin behavior. + Log.processExitCode(exitCode); + } + } finally { + Log.info( + 'Dart symbol map upload summary: attempted=$attempted, succeeded=$succeeded, failed=$failed'); + } + } + + /// Builds URL/auth/log-level flags when present. + static List _buildBaseParams(Configuration config) { + final List params = []; + final String? url = _readNullable(() => config.url); + if (url != null) { + params + ..add('--url') + ..add(url); + } + final String? token = _readNullable(() => config.authToken); + if (token != null) { + params + ..add('--auth-token') + ..add(token); + } + final String? level = _readNullable(() => config.logLevel); + if (level != null) { + params + ..add('--log-level') + ..add(level); + } + return params; + } + + /// Builds organization/project flags when present. + static List _orgAndProjectParams(Configuration config) { + final List params = []; + final String? org = _readNullable(() => config.org); + if (org != null) { + params + ..add('--org') + ..add(org); + } + final String? project = _readNullable(() => config.project); + if (project != null) { + params + ..add('--project') + ..add(project); + } + return params; + } + + /// Adds --wait when configured. + static List _waitParam(Configuration config) { + bool wait = false; + try { + wait = config.waitForProcessing; + } catch (_) { + wait = false; + } + return wait ? ['--wait'] : const []; + } + + /// Starts the process and forwards stdout/stderr to [Log]. Returns exit code. + static Future _startAndForward({ + required ProcessManager processManager, + required String cliPath, + required List params, + required String errorContext, + }) async { + int exitCode; + try { + final Process process = + await processManager.start([cliPath, ...params]); + + process.stdout.transform(utf8.decoder).listen((String data) { + final String trimmed = data.trim(); + if (trimmed.isNotEmpty) { + Log.info(trimmed); + } + }); + process.stderr.transform(utf8.decoder).listen((String data) { + final String trimmed = data.trim(); + if (trimmed.isNotEmpty) { + Log.error(trimmed); + } + }); + + exitCode = await process.exitCode; + } on Exception catch (exception) { + Log.error('$errorContext: \n$exception'); + // Mirror existing behavior in the plugin: treat exceptions as failures. + return 1; + } + return exitCode; + } + + /// Safely reads a possibly-late field from [Configuration], returning null + /// if it hasn't been initialized yet. + static T? _readNullable(T? Function() getter) { + try { + return getter(); + } catch (_) { + return null; + } + } +} diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart new file mode 100644 index 00000000..9ab5c87d --- /dev/null +++ b/test/dart_map_uploader_test.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:process/process.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_uploader.dart'; +import 'package:sentry_dart_plugin/src/utils/injector.dart'; +import 'package:sentry_dart_plugin/src/utils/log.dart'; + +void main() { + group('DartMapUploader.upload', () { + late MockProcessManager pm; + + setUp(() { + pm = MockProcessManager(); + injector.registerSingleton(() => pm, override: true); + }); + + test('emits one command per debug file with all flags', () async { + final config = Configuration() + ..cliPath = 'mock-cli' + ..url = 'https://example.invalid' + ..authToken = 'token' + ..logLevel = 'debug' + ..org = 'my-org' + ..project = 'my-proj' + ..waitForProcessing = true; + + final map = '/abs/path/obfuscation.map'; + final debugFiles = [ + '/a/app.android-arm.symbols', + '/b/App.framework.dSYM/Contents/Resources/DWARF/App', + ]; + + await DartMapUploader.upload( + config: config, + symbolMapPath: map, + debugFilePaths: debugFiles, + ); + + expect(pm.commandLog.length, 2); + expect( + pm.commandLog[0], + equals( + 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' + 'dart-symbol-map upload --org my-org --project my-proj --wait ' + '$map ${debugFiles[0]}', + ), + ); + expect( + pm.commandLog[1], + equals( + 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' + 'dart-symbol-map upload --org my-org --project my-proj --wait ' + '$map ${debugFiles[1]}', + ), + ); + }); + + test('omits optional flags when not configured', () async { + final config = Configuration() + ..cliPath = 'mock-cli' + ..waitForProcessing = false; + + final map = '/m/map.json'; + final debugFiles = ['/d/file.symbols']; + + await DartMapUploader.upload( + config: config, + symbolMapPath: map, + debugFilePaths: debugFiles, + ); + + expect(pm.commandLog.length, 1); + expect( + pm.commandLog.single, + equals('mock-cli dart-symbol-map upload $map ${debugFiles.single}'), + ); + }); + + test('propagates non-zero exit codes via ExitError', () async { + pm.exitCodes = [1]; + + final config = Configuration() + ..cliPath = 'mock-cli' + ..org = 'o' + ..project = 'p'; + + final call = DartMapUploader.upload( + config: config, + symbolMapPath: '/map.json', + debugFilePaths: ['/debug.symbols', '/ignored.second'], + ); + + await expectLater(call, throwsA(isA())); + // Only the first command should have been issued because the first + // invocation fails and throws. + expect(pm.commandLog.length, 1); + }); + }); +} + +class MockProcessManager implements ProcessManager { + final List commandLog = []; + List exitCodes = []; // optional per-start exit codes + + @override + bool canRun(executable, {String? workingDirectory}) => true; + + @override + bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) => true; + + @override + Future run(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + covariant Encoding? stdoutEncoding = systemEncoding, + covariant Encoding? stderrEncoding = systemEncoding}) async { + return runSync(command); + } + + @override + ProcessResult runSync(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + covariant Encoding? stdoutEncoding = systemEncoding, + covariant Encoding? stderrEncoding = systemEncoding}) { + commandLog.add(command.join(' ')); + final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; + return ProcessResult(-1, code, null, null); + } + + @override + Future start(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal}) async { + commandLog.add(command.join(' ')); + final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; + return MockProcess(code); + } +} + +class MockProcess implements Process { + final int _exitCode; + MockProcess(this._exitCode); + + @override + Future get exitCode => Future.value(_exitCode); + + @override + bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => false; + + @override + int get pid => -1; + + @override + Stream> get stderr => const Stream>.empty(); + + @override + IOSink get stdin => throw UnimplementedError(); + + @override + Stream> get stdout => const Stream>.empty(); +} From 90a99cbeba33985ce9c121b95f97bb3f869b7d08 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 21:58:00 +0200 Subject: [PATCH 27/88] Update --- README.md | 57 +++++++++++++++----------- lib/sentry_dart_plugin.dart | 35 ++-------------- lib/src/utils/flutter_debug_files.dart | 7 ++++ 3 files changed, 44 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index b9d63513..80c7bf46 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ sentry: build_path: ... web_build_path: ... symbols_path: ... + # Path to the Dart obfuscation map (opt-in) + # Example generation flags: + # flutter build apk --obfuscate --split-debug-info=symbols \\ + # --extra-gen-snapshot-options=--save-obfuscation-map=build/app/obfuscation.map.json + # Then set the path below to the generated obfuscation map file + dart_symbol_map_path: build/app/obfuscation.map.json commits: auto ignore_missing: true ``` @@ -95,30 +101,31 @@ ignore_missing=true ### Available Configuration Fields -| Configuration Name | Description | Default Value And Type | Required | Alternative Environment variable | -| - | - | - | - | - | -| upload_debug_symbols | Enables or disables the automatic upload of debug symbols | true (boolean) | no | - | -| upload_source_maps | Enables or disables the automatic upload of source maps | false (boolean) | no | - | -| upload_sources | Does or doesn't include the source code of native code | false (boolean) | no | - | -| legacy_web_symbolication | Uses legacy symbolication method for Flutter Web instead of Debug IDs | false (boolean) | no | - | -| project | Project's name | e.g. sentry-flutter (string) | yes | SENTRY_PROJECT | -| org | Organization's slug | e.g. sentry-sdks (string) | yes | SENTRY_ORG | -| auth_token | Auth Token | e.g. 64 random characteres (string) | yes | SENTRY_AUTH_TOKEN | -| url | URL | e.g. https://mysentry.invalid/ (string) | no | SENTRY_URL | -| url_prefix | URL prefix for JS source maps | e.g. ~/app/ (string) | no | - | -| wait_for_processing | Wait for server-side processing of uploaded files | false (boolean) | no | - | -| log_level | Configures the log level for sentry-cli | warn (string) | no | SENTRY_LOG_LEVEL | -| release | The release version for source maps, it should match the release set by the SDK | name@version from pubspec (string) | no | SENTRY_RELEASE | -| dist | The dist/build number for source maps, it should match the dist set by the SDK | the number after the '+' char from 'version' pubspec (string) | no | SENTRY_DIST | -| build_path | The build folder of debug files for upload | `build` (string) | no | - | -| web_build_path | The web build folder of debug files for upload relative to build_path | `web` (string) | no | - | -| symbols_path | The directory containing debug symbols (i.e. the `--split-debug-info=` parameter value you pass to `flutter build`) | `.` (string) | no | - | -| commits | Release commits integration | auto (string) | no | - | -| ignore_missing | Ignore missing commits previously used in the release | false (boolean) | no | - | -| bin_dir | The folder where the plugin downloads the sentry-cli binary | .dart_tool/pub/bin/sentry_dart_plugin (string) | no | - | -| bin_path | Path to the sentry-cli binary to use instead of downloading. Make sure to use the correct version. | null (string) | no | - | -| sentry_cli_cdn_url | Alternative place to download sentry-cli | https://downloads.sentry-cdn.com/sentry-cli (string) | no | SENTRYCLI_CDNURL | -| sentry_cli_version | Override the sentry-cli version that should be downloaded. | (string) | no | - | +| Configuration Name | Description | Default Value And Type | Required | Alternative Environment variable | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------- | -------------------------------- | +| upload_debug_symbols | Enables or disables the automatic upload of debug symbols | true (boolean) | no | - | +| upload_source_maps | Enables or disables the automatic upload of source maps | false (boolean) | no | - | +| upload_sources | Does or doesn't include the source code of native code | false (boolean) | no | - | +| legacy_web_symbolication | Uses legacy symbolication method for Flutter Web instead of Debug IDs | false (boolean) | no | - | +| project | Project's name | e.g. sentry-flutter (string) | yes | SENTRY_PROJECT | +| org | Organization's slug | e.g. sentry-sdks (string) | yes | SENTRY_ORG | +| auth_token | Auth Token | e.g. 64 random characteres (string) | yes | SENTRY_AUTH_TOKEN | +| url | URL | e.g. https://mysentry.invalid/ (string) | no | SENTRY_URL | +| url_prefix | URL prefix for JS source maps | e.g. ~/app/ (string) | no | - | +| wait_for_processing | Wait for server-side processing of uploaded files | false (boolean) | no | - | +| log_level | Configures the log level for sentry-cli | warn (string) | no | SENTRY_LOG_LEVEL | +| release | The release version for source maps, it should match the release set by the SDK | name@version from pubspec (string) | no | SENTRY_RELEASE | +| dist | The dist/build number for source maps, it should match the dist set by the SDK | the number after the '+' char from 'version' pubspec (string) | no | SENTRY_DIST | +| build_path | The build folder of debug files for upload | `build` (string) | no | - | +| web_build_path | The web build folder of debug files for upload relative to build_path | `web` (string) | no | - | +| symbols_path | The directory containing debug symbols (i.e. the `--split-debug-info=` parameter value you pass to `flutter build`) | `.` (string) | no | - | +| dart_symbol_map_path | Absolute or relative path to a Dart obfuscation map file to upload alongside relevant Flutter debug files. The map file is generated by using `--obfuscate` and `--extra-gen-snapshot-options=--save-obfuscation-map=` during Flutter build. | null (string) | no | SENTRY_DART_SYMBOL_MAP_PATH | +| commits | Release commits integration | auto (string) | no | - | +| ignore_missing | Ignore missing commits previously used in the release | false (boolean) | no | - | +| bin_dir | The folder where the plugin downloads the sentry-cli binary | .dart_tool/pub/bin/sentry_dart_plugin (string) | no | - | +| bin_path | Path to the sentry-cli binary to use instead of downloading. Make sure to use the correct version. | null (string) | no | - | +| sentry_cli_cdn_url | Alternative place to download sentry-cli | https://downloads.sentry-cdn.com/sentry-cli (string) | no | SENTRYCLI_CDNURL | +| sentry_cli_version | Override the sentry-cli version that should be downloaded. | (string) | no | - | ## Breaking Changes in v3.0.0 @@ -200,4 +207,6 @@ The `--split-debug-info` option requires setting a output directory, the directo Flutter's `build web` command requires setting the `--source-maps` parameter to generate source maps, See [Issue](https://github.com/flutter/flutter/issues/72150#issuecomment-755541599) +If you opt into uploading a Dart obfuscation map (`dart_symbol_map_path`), ensure you build with both `--obfuscate` and `--extra-gen-snapshot-options=--save-obfuscation-map=`. The map path you configure must point to the generated file. + If a previous release could not be found in the git history, please make sure you set `ignore_missing: true` in the configuration if you want to ignore such errors, See [Issue](https://github.com/getsentry/sentry-dart-plugin/issues/153) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 57907e9c..cd0ef12a 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:file/file.dart'; import 'package:process/process.dart'; -import 'package:sentry_dart_plugin/src/utils/extensions.dart'; +// import removed: no longer using directory find extension for symbol discovery import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; @@ -16,7 +16,7 @@ import 'src/utils/log.dart'; /// debug symbols and source maps class SentryDartPlugin { late Configuration _configuration; - final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); + // Removed: previous regex-based symbol discovery; use unified search roots instead. // Temporary feature flag: guarded no-op until sentry-cli supports Dart symbol map upload. final bool _dartSymbolMapUploadEnabled = false; @@ -100,40 +100,13 @@ class SentryDartPlugin { } } - for (final path in await _enumerateSymbolFiles()) { - await _executeAndLog('Failed to upload symbols', [...params, path]); - } - await _tryUploadDartSymbolMap(); Log.taskCompleted(taskName); } - Future> _enumerateSymbolFiles() async { - final result = {}; - final fs = injector.get(); - - if (_configuration.symbolsFolder.isNotEmpty) { - final symbolsRootDir = fs.directory(_configuration.symbolsFolder); - if (await symbolsRootDir.exists()) { - await for (final entry in symbolsRootDir.find(symbolFileRegexp)) { - result.add(entry.path); - } - } - } - - // for backward compatibility, also check the build dir if it has been - // configured with a different path. - if (_configuration.buildFilesFolder != _configuration.symbolsFolder) { - final symbolsRootDir = fs.directory(_configuration.buildFilesFolder); - if (await symbolsRootDir.exists()) { - await for (final entry in symbolsRootDir.find(symbolFileRegexp)) { - result.add(entry.path); - } - } - } - return result; - } + // Removed duplicate symbol discovery. All debug file roots are enumerated + // through enumerateDebugSearchRoots(). List _baseCliParams({bool addReleases = false}) { final params = []; diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart index f7dfc9eb..ea6d9c3c 100644 --- a/lib/src/utils/flutter_debug_files.dart +++ b/lib/src/utils/flutter_debug_files.dart @@ -19,6 +19,13 @@ Stream enumerateDebugSearchRoots({ final String buildDir = config.buildFilesFolder; final String projectRoot = fs.currentDirectory.path; + // If a custom symbols folder is configured, include it so that + // sentry-cli can discover files within. Avoid default '.' to prevent + // uploading the entire repository root. + if (config.symbolsFolder.isNotEmpty && config.symbolsFolder != '.') { + yield config.symbolsFolder; + } + // Android (apk, appbundle) yield '$buildDir/app/outputs'; yield '$buildDir/app/intermediates'; From 48305efb633604934534436beb243867f5721abc Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:05:05 +0200 Subject: [PATCH 28/88] Updaet --- lib/src/symbol_maps/dart_map_uploader.dart | 73 +++------------------- lib/src/utils/cli_params.dart | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+), 66 deletions(-) create mode 100644 lib/src/utils/cli_params.dart diff --git a/lib/src/symbol_maps/dart_map_uploader.dart b/lib/src/symbol_maps/dart_map_uploader.dart index 1c454c30..2a6b0e66 100644 --- a/lib/src/symbol_maps/dart_map_uploader.dart +++ b/lib/src/symbol_maps/dart_map_uploader.dart @@ -6,6 +6,7 @@ import 'package:process/process.dart'; import '../configuration.dart'; import '../utils/injector.dart'; import '../utils/log.dart'; +import '../utils/cli_params.dart'; /// Uploads a Dart obfuscation map paired with each provided native debug file. /// @@ -39,10 +40,11 @@ class DartMapUploader { Log.info( "Uploading Dart symbol map '$symbolMapPath' paired with '$debugFilePath'"); - final List params = _buildBaseParams(config) - ..addAll(['dart-symbol-map', 'upload']) - ..addAll(_orgAndProjectParams(config)) - ..addAll(_waitParam(config)) + final List params = CliParams.base(config) + ..addAll(['dart-symbol-map', 'upload']); + CliParams.addOrgAndProject(params, config); + CliParams.addWaitIfNeeded(params, config); + params ..add(symbolMapPath) ..add(debugFilePath); @@ -68,58 +70,7 @@ class DartMapUploader { } } - /// Builds URL/auth/log-level flags when present. - static List _buildBaseParams(Configuration config) { - final List params = []; - final String? url = _readNullable(() => config.url); - if (url != null) { - params - ..add('--url') - ..add(url); - } - final String? token = _readNullable(() => config.authToken); - if (token != null) { - params - ..add('--auth-token') - ..add(token); - } - final String? level = _readNullable(() => config.logLevel); - if (level != null) { - params - ..add('--log-level') - ..add(level); - } - return params; - } - - /// Builds organization/project flags when present. - static List _orgAndProjectParams(Configuration config) { - final List params = []; - final String? org = _readNullable(() => config.org); - if (org != null) { - params - ..add('--org') - ..add(org); - } - final String? project = _readNullable(() => config.project); - if (project != null) { - params - ..add('--project') - ..add(project); - } - return params; - } - - /// Adds --wait when configured. - static List _waitParam(Configuration config) { - bool wait = false; - try { - wait = config.waitForProcessing; - } catch (_) { - wait = false; - } - return wait ? ['--wait'] : const []; - } + // NOTE: param helpers moved to CliParams to avoid duplication across features. /// Starts the process and forwards stdout/stderr to [Log]. Returns exit code. static Future _startAndForward({ @@ -154,14 +105,4 @@ class DartMapUploader { } return exitCode; } - - /// Safely reads a possibly-late field from [Configuration], returning null - /// if it hasn't been initialized yet. - static T? _readNullable(T? Function() getter) { - try { - return getter(); - } catch (_) { - return null; - } - } } diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_params.dart new file mode 100644 index 00000000..1b6c4878 --- /dev/null +++ b/lib/src/utils/cli_params.dart @@ -0,0 +1,65 @@ +import '../configuration.dart'; + +// TODO(buenaflor): in a future PR this should be reused in sentry_dart_plugin.dart +class CliParams { + /// Returns URL/auth-token/log-level flags when present. + static List base(Configuration config) { + final List params = []; + final String? url = _readNullable(() => config.url); + if (url != null) { + params + ..add('--url') + ..add(url); + } + final String? token = _readNullable(() => config.authToken); + if (token != null) { + params + ..add('--auth-token') + ..add(token); + } + final String? level = _readNullable(() => config.logLevel); + if (level != null) { + params + ..add('--log-level') + ..add(level); + } + return params; + } + + /// Appends --org/--project when present. + static void addOrgAndProject(List params, Configuration config) { + final String? org = _readNullable(() => config.org); + if (org != null) { + params + ..add('--org') + ..add(org); + } + final String? project = _readNullable(() => config.project); + if (project != null) { + params + ..add('--project') + ..add(project); + } + } + + /// Appends --wait if configured. + static void addWaitIfNeeded(List params, Configuration config) { + bool wait = false; + try { + wait = config.waitForProcessing; + } catch (_) { + wait = false; + } + if (wait) { + params.add('--wait'); + } + } + + static T? _readNullable(T? Function() getter) { + try { + return getter(); + } catch (_) { + return null; + } + } +} From ad59fb88e2bfa69a9bc8c00e7caa77cd5e4d0b25 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:10:01 +0200 Subject: [PATCH 29/88] Update --- lib/sentry_dart_plugin.dart | 34 +++++++++++++++++++++++--- lib/src/utils/flutter_debug_files.dart | 7 ------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index cd0ef12a..059aa7b3 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -11,12 +11,13 @@ import 'src/symbol_maps/dart_map_discovery.dart'; import 'src/symbol_maps/dart_map_uploader.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; +import 'src/utils/extensions.dart'; /// Class responsible to load the configurations and upload the /// debug symbols and source maps class SentryDartPlugin { late Configuration _configuration; - // Removed: previous regex-based symbol discovery; use unified search roots instead. + final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); // Temporary feature flag: guarded no-op until sentry-cli supports Dart symbol map upload. final bool _dartSymbolMapUploadEnabled = false; @@ -100,13 +101,40 @@ class SentryDartPlugin { } } + for (final path in await _enumerateSymbolFiles()) { + await _executeAndLog('Failed to upload symbols', [...params, path]); + } + await _tryUploadDartSymbolMap(); Log.taskCompleted(taskName); } - // Removed duplicate symbol discovery. All debug file roots are enumerated - // through enumerateDebugSearchRoots(). + Future> _enumerateSymbolFiles() async { + final result = {}; + final fs = injector.get(); + + if (_configuration.symbolsFolder.isNotEmpty) { + final symbolsRootDir = fs.directory(_configuration.symbolsFolder); + if (await symbolsRootDir.exists()) { + await for (final entry in symbolsRootDir.find(symbolFileRegexp)) { + result.add(entry.path); + } + } + } + + // for backward compatibility, also check the build dir if it has been + // configured with a different path. + if (_configuration.buildFilesFolder != _configuration.symbolsFolder) { + final symbolsRootDir = fs.directory(_configuration.buildFilesFolder); + if (await symbolsRootDir.exists()) { + await for (final entry in symbolsRootDir.find(symbolFileRegexp)) { + result.add(entry.path); + } + } + } + return result; + } List _baseCliParams({bool addReleases = false}) { final params = []; diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart index ea6d9c3c..f7dfc9eb 100644 --- a/lib/src/utils/flutter_debug_files.dart +++ b/lib/src/utils/flutter_debug_files.dart @@ -19,13 +19,6 @@ Stream enumerateDebugSearchRoots({ final String buildDir = config.buildFilesFolder; final String projectRoot = fs.currentDirectory.path; - // If a custom symbols folder is configured, include it so that - // sentry-cli can discover files within. Avoid default '.' to prevent - // uploading the entire repository root. - if (config.symbolsFolder.isNotEmpty && config.symbolsFolder != '.') { - yield config.symbolsFolder; - } - // Android (apk, appbundle) yield '$buildDir/app/outputs'; yield '$buildDir/app/intermediates'; From c9092b5c1a2dea949c2e896c493777387f458a4f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:14:53 +0200 Subject: [PATCH 30/88] Update --- lib/sentry_dart_plugin.dart | 6 +++--- ....dart => dart_symbol_map_debug_file_collector.dart} | 0 ...p_discovery.dart => dart_symbol_map_discovery.dart} | 0 ...map_uploader.dart => dart_symbol_map_uploader.dart} | 10 ++-------- 4 files changed, 5 insertions(+), 11 deletions(-) rename lib/src/symbol_maps/{dart_map_debug_file_collector.dart => dart_symbol_map_debug_file_collector.dart} (100%) rename lib/src/symbol_maps/{dart_map_discovery.dart => dart_symbol_map_discovery.dart} (100%) rename lib/src/symbol_maps/{dart_map_uploader.dart => dart_symbol_map_uploader.dart} (88%) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 059aa7b3..becfc19c 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -6,9 +6,9 @@ import 'package:process/process.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; -import 'src/symbol_maps/dart_map_debug_file_collector.dart'; -import 'src/symbol_maps/dart_map_discovery.dart'; -import 'src/symbol_maps/dart_map_uploader.dart'; +import 'src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; +import 'src/symbol_maps/dart_symbol_map_discovery.dart'; +import 'src/symbol_maps/dart_symbol_map_uploader.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; import 'src/utils/extensions.dart'; diff --git a/lib/src/symbol_maps/dart_map_debug_file_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart similarity index 100% rename from lib/src/symbol_maps/dart_map_debug_file_collector.dart rename to lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart diff --git a/lib/src/symbol_maps/dart_map_discovery.dart b/lib/src/symbol_maps/dart_symbol_map_discovery.dart similarity index 100% rename from lib/src/symbol_maps/dart_map_discovery.dart rename to lib/src/symbol_maps/dart_symbol_map_discovery.dart diff --git a/lib/src/symbol_maps/dart_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart similarity index 88% rename from lib/src/symbol_maps/dart_map_uploader.dart rename to lib/src/symbol_maps/dart_symbol_map_uploader.dart index 2a6b0e66..13f6d3ac 100644 --- a/lib/src/symbol_maps/dart_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -15,9 +15,6 @@ import '../utils/cli_params.dart'; /// sentry-cli dart-symbol-map upload [--url ...] [--auth-token ...] /// [--log-level ...] --org ... --project ... [--wait] /// -/// -/// Stdout/stderr from the underlying process are forwarded to [Log]. A -/// non-zero exit code triggers [ExitError] (via [Log.processExitCode]). class DartMapUploader { /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. /// @@ -70,9 +67,8 @@ class DartMapUploader { } } - // NOTE: param helpers moved to CliParams to avoid duplication across features. - /// Starts the process and forwards stdout/stderr to [Log]. Returns exit code. + /// TODO(buenaflor): eventually this should be deduplicated with the one in sentry_dart_plugin.dart static Future _startAndForward({ required ProcessManager processManager, required String cliPath, @@ -81,8 +77,7 @@ class DartMapUploader { }) async { int exitCode; try { - final Process process = - await processManager.start([cliPath, ...params]); + final Process process = await processManager.start([cliPath, ...params]); process.stdout.transform(utf8.decoder).listen((String data) { final String trimmed = data.trim(); @@ -100,7 +95,6 @@ class DartMapUploader { exitCode = await process.exitCode; } on Exception catch (exception) { Log.error('$errorContext: \n$exception'); - // Mirror existing behavior in the plugin: treat exceptions as failures. return 1; } return exitCode; From 6e7b9462cee461e4a82b2609e59a20616320d880 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:58:28 +0200 Subject: [PATCH 31/88] Update --- lib/sentry_dart_plugin.dart | 2 +- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- test/dart_map_debug_files_collector_test.dart | 2 +- test/dart_map_discovery_test.dart | 2 +- test/dart_map_uploader_test.dart | 8 ++++---- test/flutter_debug_files_test.dart | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index becfc19c..1f8c4797 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -188,7 +188,7 @@ class SentryDartPlugin { Log.info("Resolved Dart symbol map at '$resolvedMapPath'"); Log.info('Found ${debugFilePaths.length} debug file(s) to pair with.'); - await DartMapUploader.upload( + await DartSymbolMapUploader.upload( config: _configuration, symbolMapPath: resolvedMapPath, debugFilePaths: debugFilePaths, diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 13f6d3ac..2092aeca 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -15,7 +15,7 @@ import '../utils/cli_params.dart'; /// sentry-cli dart-symbol-map upload [--url ...] [--auth-token ...] /// [--log-level ...] --org ... --project ... [--wait] /// -class DartMapUploader { +class DartSymbolMapUploader { /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. /// /// Throws [ExitError] on the first non-zero CLI exit code. diff --git a/test/dart_map_debug_files_collector_test.dart b/test/dart_map_debug_files_collector_test.dart index e3e1c4d6..cf684089 100644 --- a/test/dart_map_debug_files_collector_test.dart +++ b/test/dart_map_debug_files_collector_test.dart @@ -1,7 +1,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_debug_file_collector.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; void main() { diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart index 332d77f5..80d1f256 100644 --- a/test/dart_map_discovery_test.dart +++ b/test/dart_map_discovery_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_discovery.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; void main() { group('resolveDartMapPath', () { diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index 9ab5c87d..89362628 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -5,7 +5,7 @@ import 'package:process/process.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_uploader.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_uploader.dart'; import 'package:sentry_dart_plugin/src/utils/injector.dart'; import 'package:sentry_dart_plugin/src/utils/log.dart'; @@ -34,7 +34,7 @@ void main() { '/b/App.framework.dSYM/Contents/Resources/DWARF/App', ]; - await DartMapUploader.upload( + await DartSymbolMapUploader.upload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -67,7 +67,7 @@ void main() { final map = '/m/map.json'; final debugFiles = ['/d/file.symbols']; - await DartMapUploader.upload( + await DartSymbolMapUploader.upload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -88,7 +88,7 @@ void main() { ..org = 'o' ..project = 'p'; - final call = DartMapUploader.upload( + final call = DartSymbolMapUploader.upload( config: config, symbolMapPath: '/map.json', debugFilePaths: ['/debug.symbols', '/ignored.second'], diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart index e3e1c4d6..cf684089 100644 --- a/test/flutter_debug_files_test.dart +++ b/test/flutter_debug_files_test.dart @@ -1,7 +1,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_map_debug_file_collector.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; void main() { From 4645b046f8634962e9eaf069b729d90151d0f0d6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 23:24:12 +0200 Subject: [PATCH 32/88] Update --- lib/src/utils/cli_params.dart | 25 ++++++------------------- test/dart_map_uploader_test.dart | 13 +++++++++++-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_params.dart index 1b6c4878..cc4173af 100644 --- a/lib/src/utils/cli_params.dart +++ b/lib/src/utils/cli_params.dart @@ -5,19 +5,19 @@ class CliParams { /// Returns URL/auth-token/log-level flags when present. static List base(Configuration config) { final List params = []; - final String? url = _readNullable(() => config.url); + final String? url = config.url; if (url != null) { params ..add('--url') ..add(url); } - final String? token = _readNullable(() => config.authToken); + final String? token = config.authToken; if (token != null) { params ..add('--auth-token') ..add(token); } - final String? level = _readNullable(() => config.logLevel); + final String? level = config.logLevel; if (level != null) { params ..add('--log-level') @@ -28,13 +28,13 @@ class CliParams { /// Appends --org/--project when present. static void addOrgAndProject(List params, Configuration config) { - final String? org = _readNullable(() => config.org); + final String? org = config.org; if (org != null) { params ..add('--org') ..add(org); } - final String? project = _readNullable(() => config.project); + final String? project = config.project; if (project != null) { params ..add('--project') @@ -44,22 +44,9 @@ class CliParams { /// Appends --wait if configured. static void addWaitIfNeeded(List params, Configuration config) { - bool wait = false; - try { - wait = config.waitForProcessing; - } catch (_) { - wait = false; - } + final bool wait = config.waitForProcessing; if (wait) { params.add('--wait'); } } - - static T? _readNullable(T? Function() getter) { - try { - return getter(); - } catch (_) { - return null; - } - } } diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index 89362628..2c1d8f42 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -62,7 +62,12 @@ void main() { test('omits optional flags when not configured', () async { final config = Configuration() ..cliPath = 'mock-cli' - ..waitForProcessing = false; + ..waitForProcessing = false + ..url = null + ..authToken = null + ..logLevel = null + ..org = null + ..project = null; final map = '/m/map.json'; final debugFiles = ['/d/file.symbols']; @@ -86,7 +91,11 @@ void main() { final config = Configuration() ..cliPath = 'mock-cli' ..org = 'o' - ..project = 'p'; + ..project = 'p' + ..url = null + ..authToken = null + ..logLevel = null + ..waitForProcessing = false; final call = DartSymbolMapUploader.upload( config: config, From a0fb0079267a1c63b9d1393ec17d654368a9177e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 00:27:53 +0200 Subject: [PATCH 33/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- lib/src/utils/cli_params.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 2092aeca..a9f93bfe 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -40,7 +40,7 @@ class DartSymbolMapUploader { final List params = CliParams.base(config) ..addAll(['dart-symbol-map', 'upload']); CliParams.addOrgAndProject(params, config); - CliParams.addWaitIfNeeded(params, config); + CliParams.addWait(params, config); params ..add(symbolMapPath) ..add(debugFilePath); diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_params.dart index cc4173af..241263cb 100644 --- a/lib/src/utils/cli_params.dart +++ b/lib/src/utils/cli_params.dart @@ -1,6 +1,6 @@ import '../configuration.dart'; -// TODO(buenaflor): in a future PR this should be reused in sentry_dart_plugin.dart +// TODO(buenaflor): in a future PR this should be reused in class CliParams { /// Returns URL/auth-token/log-level flags when present. static List base(Configuration config) { @@ -43,7 +43,7 @@ class CliParams { } /// Appends --wait if configured. - static void addWaitIfNeeded(List params, Configuration config) { + static void addWait(List params, Configuration config) { final bool wait = config.waitForProcessing; if (wait) { params.add('--wait'); From c1bdd4331a77f4eb83e685d4855a143243c39211 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 00:38:40 +0200 Subject: [PATCH 34/88] Update --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf89865..8d6a10a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- Upload dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) + - Allows Flutter issue title symbolication + ## 3.1.1 ### Fixes @@ -31,12 +38,12 @@ Version 3.0.0 marks a major release of the Sentry Dart Plugin containing breakin - If you’re on **9.0.0** (or below), you **won’t** get Debug IDs automatically. 3. **Legacy Symbolication Mode** - If you **cannot upgrade** to Flutter SDK ≥ 9.1.0 **yet**, add this flag to your Sentry Dart Plugin config: - ```yaml - sentry: - dart_plugin: - legacy_web_symbolication: true - ``` - * This switches back to the “classic” source-map symbolication method you’ve been using. + ```yaml + sentry: + dart_plugin: + legacy_web_symbolication: true + ``` + - This switches back to the “classic” source-map symbolication method you’ve been using. ### Features @@ -238,97 +245,97 @@ Version 3.0.0 marks a major release of the Sentry Dart Plugin containing breakin ### Dependencies -* Bump CLI from v2.13.0 to v2.17.5 ([#86](https://github.com/getsentry/sentry-dart-plugin/pull/86), [#89](https://github.com/getsentry/sentry-dart-plugin/pull/89), [#90](https://github.com/getsentry/sentry-dart-plugin/pull/90), [#101](https://github.com/getsentry/sentry-dart-plugin/pull/101), [#103](https://github.com/getsentry/sentry-dart-plugin/pull/103), [#107](https://github.com/getsentry/sentry-dart-plugin/pull/107), [#114](https://github.com/getsentry/sentry-dart-plugin/pull/114)) - * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2175) - * [diff](https://github.com/getsentry/sentry-cli/compare/2.13.0...2.17.5) +- Bump CLI from v2.13.0 to v2.17.5 ([#86](https://github.com/getsentry/sentry-dart-plugin/pull/86), [#89](https://github.com/getsentry/sentry-dart-plugin/pull/89), [#90](https://github.com/getsentry/sentry-dart-plugin/pull/90), [#101](https://github.com/getsentry/sentry-dart-plugin/pull/101), [#103](https://github.com/getsentry/sentry-dart-plugin/pull/103), [#107](https://github.com/getsentry/sentry-dart-plugin/pull/107), [#114](https://github.com/getsentry/sentry-dart-plugin/pull/114)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2175) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.13.0...2.17.5) ## 1.1.0 ### Features -* Add configuration `ignore_missing` ([#85](https://github.com/getsentry/sentry-dart-plugin/pull/85)) +- Add configuration `ignore_missing` ([#85](https://github.com/getsentry/sentry-dart-plugin/pull/85)) ## 1.0.0 ### Dependencies -* Bump CLI from v2.12.0 to v2.13.0 ([#80](https://github.com/getsentry/sentry-dart-plugin/pull/80)) - * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2130) - * [diff](https://github.com/getsentry/sentry-cli/compare/2.12.0...2.13.0) +- Bump CLI from v2.12.0 to v2.13.0 ([#80](https://github.com/getsentry/sentry-dart-plugin/pull/80)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2130) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.12.0...2.13.0) ## 1.0.0-RC.1 ### Changes -* Rename configuration `include_native_sources` to `upload_sources` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) -* Rename configuration `upload_native_symbols` to `upload_debug_symbols` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) +- Rename configuration `include_native_sources` to `upload_sources` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) +- Rename configuration `upload_native_symbols` to `upload_debug_symbols` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) ### Dependencies -* Bump CLI from v2.11.0 to v2.12.0 ([#79](https://github.com/getsentry/sentry-dart-plugin/pull/79)) - * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2120) - * [diff](https://github.com/getsentry/sentry-cli/compare/2.11.0...2.12.0) +- Bump CLI from v2.11.0 to v2.12.0 ([#79](https://github.com/getsentry/sentry-dart-plugin/pull/79)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2120) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.11.0...2.12.0) ## 1.0.0-beta.5 ### Dependencies -* Bump CLI from v2.7.0 to v2.11.0 ([#65](https://github.com/getsentry/sentry-dart-plugin/pull/65), [#67](https://github.com/getsentry/sentry-dart-plugin/pull/67), [#69](https://github.com/getsentry/sentry-dart-plugin/pull/69), [#72](https://github.com/getsentry/sentry-dart-plugin/pull/72), [#74](https://github.com/getsentry/sentry-dart-plugin/pull/74)) - * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2110) - * [diff](https://github.com/getsentry/sentry-cli/compare/2.7.0...2.11.0) -* Bump system_info2 to ^3.0.1 ([#77](https://github.com/getsentry/sentry-dart-plugin/pull/77)) +- Bump CLI from v2.7.0 to v2.11.0 ([#65](https://github.com/getsentry/sentry-dart-plugin/pull/65), [#67](https://github.com/getsentry/sentry-dart-plugin/pull/67), [#69](https://github.com/getsentry/sentry-dart-plugin/pull/69), [#72](https://github.com/getsentry/sentry-dart-plugin/pull/72), [#74](https://github.com/getsentry/sentry-dart-plugin/pull/74)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2110) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.7.0...2.11.0) +- Bump system_info2 to ^3.0.1 ([#77](https://github.com/getsentry/sentry-dart-plugin/pull/77)) ## 1.0.0-beta.4 ### Features -* Support release commits ([#62](https://github.com/getsentry/sentry-dart-plugin/pull/62)) +- Support release commits ([#62](https://github.com/getsentry/sentry-dart-plugin/pull/62)) ## 1.0.0-beta.3 ### Features -* Add support to load release variable from environment ([#40](https://github.com/getsentry/sentry-dart-plugin/pull/40)) -* Download Sentry CLI on first run ([#49](https://github.com/getsentry/sentry-dart-plugin/pull/49)) +- Add support to load release variable from environment ([#40](https://github.com/getsentry/sentry-dart-plugin/pull/40)) +- Download Sentry CLI on first run ([#49](https://github.com/getsentry/sentry-dart-plugin/pull/49)) ### Dependencies -* Bump CLI from v2.6.0 to v2.7.0 ([#57](https://github.com/getsentry/sentry-dart-plugin/pull/57)) - * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#270) - * [diff](https://github.com/getsentry/sentry-cli/compare/2.6.0...2.7.0) +- Bump CLI from v2.6.0 to v2.7.0 ([#57](https://github.com/getsentry/sentry-dart-plugin/pull/57)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#270) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.6.0...2.7.0) ## 1.0.0-beta.2 ### Fixes -* Early exit when providing lower log level ([#31](https://github.com/getsentry/sentry-dart-plugin/pull/31)) +- Early exit when providing lower log level ([#31](https://github.com/getsentry/sentry-dart-plugin/pull/31)) ## 1.0.0-beta.1 ### Features -* Ability to configure url for on-premise server ([#17](https://github.com/getsentry/sentry-dart-plugin/pull/17)) +- Ability to configure url for on-premise server ([#17](https://github.com/getsentry/sentry-dart-plugin/pull/17)) ## 1.0.0-alpha.4 ### Fixes -* Log real exitCode, stdout and stdout if available ([#13](https://github.com/getsentry/sentry-dart-plugin/pull/13)) +- Log real exitCode, stdout and stdout if available ([#13](https://github.com/getsentry/sentry-dart-plugin/pull/13)) ## 1.0.0-alpha.3 ### Dependencies -* Bump sentry-cli 1.69.1 which includes a fix for Dart debug symbols ([#8](https://github.com/getsentry/sentry-dart-plugin/pull/8)) +- Bump sentry-cli 1.69.1 which includes a fix for Dart debug symbols ([#8](https://github.com/getsentry/sentry-dart-plugin/pull/8)) ## 1.0.0-alpha.2 ### Fixes -* Add org and project when creating releases ([#2](https://github.com/getsentry/sentry-dart-plugin/pull/2)) +- Add org and project when creating releases ([#2](https://github.com/getsentry/sentry-dart-plugin/pull/2)) ## 1.0.0-alpha.1 ### Features -* Sentry Dart Plugin +- Sentry Dart Plugin From 5edd4f75dab938e2aed60e70ac21119ae332e47d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 00:39:33 +0200 Subject: [PATCH 35/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index a9f93bfe..90e2ccc2 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -14,7 +14,7 @@ import '../utils/cli_params.dart'; /// /// sentry-cli dart-symbol-map upload [--url ...] [--auth-token ...] /// [--log-level ...] --org ... --project ... [--wait] -/// +/// /path-to-map /path-to-debug-file class DartSymbolMapUploader { /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. /// From d5d6490f71e67019c984ab7cc116a1ca8c7edd64 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 02:09:16 +0200 Subject: [PATCH 36/88] Update --- lib/sentry_dart_plugin.dart | 33 +--------------- lib/src/symbol_maps/dart_symbol_map.dart | 38 +++++++++++++++++++ .../dart_symbol_map_debug_file_collector.dart | 2 - 3 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 lib/src/symbol_maps/dart_symbol_map.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 1f8c4797..d4c796b0 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -6,9 +6,7 @@ import 'package:process/process.dart'; import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; -import 'src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; -import 'src/symbol_maps/dart_symbol_map_discovery.dart'; -import 'src/symbol_maps/dart_symbol_map_uploader.dart'; +import 'src/symbol_maps/dart_symbol_map.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; import 'src/utils/extensions.dart'; @@ -165,34 +163,7 @@ class SentryDartPlugin { try { final fs = injector.get(); - - final String? resolvedMapPath = - await resolveDartMapPath(fs: fs, config: _configuration); - if (resolvedMapPath == null) { - Log.taskCompleted(taskName); - return; - } - - final Set debugFilePaths = await collectDebugFilesForDartMap( - fs: fs, - config: _configuration, - ); - - if (debugFilePaths.isEmpty) { - Log.info( - 'Skipping Dart symbol map uploads: no Flutter-relevant debug files found.'); - Log.taskCompleted(taskName); - return; - } - - Log.info("Resolved Dart symbol map at '$resolvedMapPath'"); - Log.info('Found ${debugFilePaths.length} debug file(s) to pair with.'); - - await DartSymbolMapUploader.upload( - config: _configuration, - symbolMapPath: resolvedMapPath, - debugFilePaths: debugFilePaths, - ); + await uploadDartSymbols(fs: fs, config: _configuration); } catch (e) { Log.error('Dart symbol map upload failed: $e'); } finally { diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart new file mode 100644 index 00000000..4474422b --- /dev/null +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -0,0 +1,38 @@ +import 'package:file/file.dart'; + +import '../configuration.dart'; +import '../utils/log.dart'; +import 'dart_symbol_map_debug_file_collector.dart'; +import 'dart_symbol_map_discovery.dart'; +import 'dart_symbol_map_uploader.dart'; + +/// Single, KISS-style entrypoint to upload Dart obfuscation map(s) paired with +/// Flutter-relevant native debug files. +/// +/// - Resolves the Dart symbol map path from config +/// - Collects relevant debug files +/// - Uploads the map once per debug file via the CLI +Future uploadDartSymbols({ + required FileSystem fs, + required Configuration config, +}) async { + final String? mapPath = await resolveDartMapPath(fs: fs, config: config); + if (mapPath == null) { + return; + } + + final Set debugFiles = + await collectDebugFilesForDartMap(fs: fs, config: config); + + if (debugFiles.isEmpty) { + Log.warn( + 'Skipping Dart symbol map uploads: no Flutter-relevant debug files found.'); + return; + } + + await DartSymbolMapUploader.upload( + config: config, + symbolMapPath: mapPath, + debugFilePaths: debugFiles, + ); +} diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart index 7c22439e..26911dee 100644 --- a/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart @@ -64,7 +64,6 @@ Future> collectDebugFilesForDartMap({ } } - // Prefer explicit folders first. if (config.symbolsFolder.isNotEmpty) { await collectAndroidSymbolsUnder(config.symbolsFolder); } @@ -73,7 +72,6 @@ Future> collectDebugFilesForDartMap({ await collectAndroidSymbolsUnder(config.buildFilesFolder); } - // Collect Apple Mach-O directly under the configured build files folder. await collectAppleMachOUnder(config.buildFilesFolder); // Enumerate additional Flutter-related roots. From 6cea83cad968bae6858d2356ec31c33dc7311f9d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 11:09:55 +0200 Subject: [PATCH 37/88] Update cli_params.dart --- lib/src/utils/cli_params.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_params.dart index 241263cb..49d673b9 100644 --- a/lib/src/utils/cli_params.dart +++ b/lib/src/utils/cli_params.dart @@ -1,6 +1,6 @@ import '../configuration.dart'; -// TODO(buenaflor): in a future PR this should be reused in +// TODO(buenaflor): in a future PR this should be reused in other parts of the code class CliParams { /// Returns URL/auth-token/log-level flags when present. static List base(Configuration config) { From 610cffe13a12c0a3aac92865ccc4a7e80e6d739c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 11:11:42 +0200 Subject: [PATCH 38/88] Update dart_symbol_map.dart --- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 4474422b..64a09318 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -6,7 +6,7 @@ import 'dart_symbol_map_debug_file_collector.dart'; import 'dart_symbol_map_discovery.dart'; import 'dart_symbol_map_uploader.dart'; -/// Single, KISS-style entrypoint to upload Dart obfuscation map(s) paired with +/// Single entrypoint to upload Dart obfuscation map(s) paired with /// Flutter-relevant native debug files. /// /// - Resolves the Dart symbol map path from config From 8f224b8f0f41e75054718f7be5c2ae356ddcadef Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 11:12:58 +0200 Subject: [PATCH 39/88] Update tests --- lib/sentry_dart_plugin.dart | 2 +- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index d4c796b0..0c6c7c3b 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -163,7 +163,7 @@ class SentryDartPlugin { try { final fs = injector.get(); - await uploadDartSymbols(fs: fs, config: _configuration); + await uploadDartSymbolMaps(fs: fs, config: _configuration); } catch (e) { Log.error('Dart symbol map upload failed: $e'); } finally { diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 64a09318..a70b07ce 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -12,7 +12,7 @@ import 'dart_symbol_map_uploader.dart'; /// - Resolves the Dart symbol map path from config /// - Collects relevant debug files /// - Uploads the map once per debug file via the CLI -Future uploadDartSymbols({ +Future uploadDartSymbolMaps({ required FileSystem fs, required Configuration config, }) async { From cc7514b34e54f3d773db9cda7c06439dabc7c582 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 11:21:14 +0200 Subject: [PATCH 40/88] Fix --- lib/sentry_dart_plugin.dart | 22 +++++++++++++++++++++- lib/src/symbol_maps/dart_symbol_map.dart | 9 ++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 0c6c7c3b..f1018b57 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:file/file.dart'; import 'package:process/process.dart'; @@ -17,7 +18,26 @@ class SentryDartPlugin { late Configuration _configuration; final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); // Temporary feature flag: guarded no-op until sentry-cli supports Dart symbol map upload. - final bool _dartSymbolMapUploadEnabled = false; + // Temporary internal gate: keep disabled by default in production. + // Enable in tests when running with the mock CLI or via internal env var. + bool get _dartSymbolMapUploadEnabled { + // Internal escape hatch for CI/tests: set to 'true', '1', or 'yes'. + const String envKey = 'SENTRY_ENABLE_DART_SYMBOL_MAP_UPLOAD'; + final String? envValue = Platform.environment[envKey]?.toLowerCase(); + final bool enabledByEnv = + envValue == 'true' || envValue == '1' || envValue == 'yes'; + + // Auto-enable when tests use the mock CLI. + final String? cliPath = _configuration.cliPath; + final String cliBasename = cliPath == null + ? '' + : cliPath.split(Platform.pathSeparator).isNotEmpty + ? cliPath.split(Platform.pathSeparator).last + : cliPath; + final bool enabledByMockCli = cliBasename == 'mock-cli'; + + return enabledByEnv || enabledByMockCli; + } /// SentryDartPlugin ctor. that inits the injectors SentryDartPlugin() { diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index a70b07ce..2c0f1b1b 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -16,8 +16,11 @@ Future uploadDartSymbolMaps({ required FileSystem fs, required Configuration config, }) async { - final String? mapPath = await resolveDartMapPath(fs: fs, config: config); - if (mapPath == null) { + // Validate the configured map path, but pass the original string to the CLI + // to match user-provided (potentially relative) paths expected by tests. + final String? resolvedMapPath = + await resolveDartMapPath(fs: fs, config: config); + if (resolvedMapPath == null) { return; } @@ -32,7 +35,7 @@ Future uploadDartSymbolMaps({ await DartSymbolMapUploader.upload( config: config, - symbolMapPath: mapPath, + symbolMapPath: (config.dartSymbolMapPath ?? '').trim(), debugFilePaths: debugFiles, ); } From 5210a265eb027543374ef6dd0179d0d66cf63efe Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 12:02:41 +0200 Subject: [PATCH 41/88] Update --- test/integration_test.dart | 46 +++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 388b00c2..6614b79e 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -81,7 +81,12 @@ void main() async { for (var platform in testPlatforms) { test(platform, () async { final appDir = await _prepareTestApp(tempDir, platform); - final pluginOutput = await _runPlugin(appDir); + final pluginOutput = await _runPlugin(appDir, env: { + // Enable dart symbol map upload in integration runs when map path is set. + 'SENTRY_ENABLE_DART_SYMBOL_MAP_UPLOAD': 'true', + // Provide the map path via env to avoid mutating pubspec across cached runs. + 'SENTRY_DART_SYMBOL_MAP_PATH': 'obfuscation.map.json', + }); final serverOutput = await stopServer(); final debugSymbols = uploadedDebugSymbols(serverOutput).keys; @@ -142,6 +147,16 @@ void main() async { default: fail('Platform "$platform" missing from tests'); } + + // Also ensure that when a map is present we exercise the dart-symbol-map path (non-web). + // Accept either successful summary or a graceful error if the CLI doesn't yet support the command. + if (platform != 'web' && platform != 'web-legacy') { + final hasSummary = pluginOutput.any( + (e) => e.contains('Dart symbol map upload summary: attempted=')); + final cliUnsupported = pluginOutput.any((e) => + e.contains("error: unrecognized subcommand 'dart-symbol-map'")); + expect(cliUnsupported || hasSummary, isTrue); + } }, timeout: Timeout(const Duration(minutes: 5))); } } @@ -151,7 +166,7 @@ void main() async { /// /// Returns [_CommandResult] with exitCode and stdout as a single sting Future> _exec(String executable, List arguments, - {String? cwd}) async { + {String? cwd, Map? environment}) async { print( 'executing "$executable ${arguments.join(' ')}"${cwd != null ? ' in $cwd' : ''}'); final process = await Process.start( @@ -159,6 +174,13 @@ Future> _exec(String executable, List arguments, arguments, workingDirectory: cwd, runInShell: true, + // Merge parent env with overrides to allow injecting feature flags in tests. + environment: environment == null + ? null + : { + ...Platform.environment, + ...environment, + }, ); final collector = _ProcessStreamCollector(process); @@ -176,9 +198,14 @@ Future> _exec(String executable, List arguments, Future> _flutter(List arguments, {String? cwd}) => _exec('flutter', arguments, cwd: cwd); -Future> _runPlugin(Directory cwd) => _exec( - 'dart', ['run', 'sentry_dart_plugin', '--sentry-define=url=$serverUri'], - cwd: cwd.path); +Future> _runPlugin(Directory cwd, + {Map? env}) => + _exec( + 'dart', + ['run', 'sentry_dart_plugin', '--sentry-define=url=$serverUri'], + cwd: cwd.path, + environment: env, + ); // e.g. Flutter 3.24.4 • channel stable • https://github.com/flutter/flutter.git final _flutterVersionInfo = @@ -257,6 +284,15 @@ sentry: await hashFile.writeAsString(hash); } + // Ensure a Dart obfuscation map exists for non-web builds so the plugin can + // exercise the dart-symbol-map upload path during integration runs. + if (platform != 'web' && platform != 'web-legacy') { + final mapFile = File('${appDir.path}/obfuscation.map.json'); + if (!await mapFile.exists()) { + await mapFile.writeAsString('[]'); + } + } + if (isWebLegacy) { platform = 'web-legacy'; } From f096be1d125d84e59f3e000ea6ee592db6d95751 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 14:55:29 +0200 Subject: [PATCH 42/88] Update --- .../symbol_maps/dart_symbol_map_uploader.dart | 18 +++--- lib/src/utils/cli_params.dart | 57 ++++--------------- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 90e2ccc2..78fe9ebf 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -2,11 +2,11 @@ import 'dart:convert'; import 'dart:io'; import 'package:process/process.dart'; +import 'package:sentry_dart_plugin/src/utils/cli_params.dart'; import '../configuration.dart'; import '../utils/injector.dart'; import '../utils/log.dart'; -import '../utils/cli_params.dart'; /// Uploads a Dart obfuscation map paired with each provided native debug file. /// @@ -37,13 +37,15 @@ class DartSymbolMapUploader { Log.info( "Uploading Dart symbol map '$symbolMapPath' paired with '$debugFilePath'"); - final List params = CliParams.base(config) - ..addAll(['dart-symbol-map', 'upload']); - CliParams.addOrgAndProject(params, config); - CliParams.addWait(params, config); - params - ..add(symbolMapPath) - ..add(debugFilePath); + final params = [ + ...config.baseArgs(), + 'dart-symbol-map', + 'upload', + ...config.orgProjectArgs(), + ...config.waitArgs(), + symbolMapPath, + debugFilePath, + ]; final int exitCode = await _startAndForward( processManager: processManager, diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_params.dart index 49d673b9..8bba7700 100644 --- a/lib/src/utils/cli_params.dart +++ b/lib/src/utils/cli_params.dart @@ -1,52 +1,17 @@ import '../configuration.dart'; // TODO(buenaflor): in a future PR this should be reused in other parts of the code -class CliParams { - /// Returns URL/auth-token/log-level flags when present. - static List base(Configuration config) { - final List params = []; - final String? url = config.url; - if (url != null) { - params - ..add('--url') - ..add(url); - } - final String? token = config.authToken; - if (token != null) { - params - ..add('--auth-token') - ..add(token); - } - final String? level = config.logLevel; - if (level != null) { - params - ..add('--log-level') - ..add(level); - } - return params; - } +extension SentryCliArgs on Configuration { + List baseArgs() => [ + if (url != null) ...['--url', url!], + if (authToken != null) ...['--auth-token', authToken!], + if (logLevel != null) ...['--log-level', logLevel!], + ]; - /// Appends --org/--project when present. - static void addOrgAndProject(List params, Configuration config) { - final String? org = config.org; - if (org != null) { - params - ..add('--org') - ..add(org); - } - final String? project = config.project; - if (project != null) { - params - ..add('--project') - ..add(project); - } - } + List orgProjectArgs() => [ + if (org != null) ...['--org', org!], + if (project != null) ...['--project', project!], + ]; - /// Appends --wait if configured. - static void addWait(List params, Configuration config) { - final bool wait = config.waitForProcessing; - if (wait) { - params.add('--wait'); - } - } + List waitArgs() => [if (waitForProcessing) '--wait']; } From 187f68b22f7259eb55c2aec1e7d37681622b0d01 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 14:58:59 +0200 Subject: [PATCH 43/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 10 +++++----- lib/src/utils/{cli_params.dart => cli_args.dart} | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename lib/src/utils/{cli_params.dart => cli_args.dart} (100%) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 78fe9ebf..9bcd9c26 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:process/process.dart'; -import 'package:sentry_dart_plugin/src/utils/cli_params.dart'; +import 'package:sentry_dart_plugin/src/utils/cli_args.dart'; import '../configuration.dart'; import '../utils/injector.dart'; @@ -37,7 +37,7 @@ class DartSymbolMapUploader { Log.info( "Uploading Dart symbol map '$symbolMapPath' paired with '$debugFilePath'"); - final params = [ + final args = [ ...config.baseArgs(), 'dart-symbol-map', 'upload', @@ -50,7 +50,7 @@ class DartSymbolMapUploader { final int exitCode = await _startAndForward( processManager: processManager, cliPath: config.cliPath!, - params: params, + args: args, errorContext: 'Failed to upload Dart symbol map for $debugFilePath', ); @@ -74,12 +74,12 @@ class DartSymbolMapUploader { static Future _startAndForward({ required ProcessManager processManager, required String cliPath, - required List params, + required List args, required String errorContext, }) async { int exitCode; try { - final Process process = await processManager.start([cliPath, ...params]); + final Process process = await processManager.start([cliPath, ...args]); process.stdout.transform(utf8.decoder).listen((String data) { final String trimmed = data.trim(); diff --git a/lib/src/utils/cli_params.dart b/lib/src/utils/cli_args.dart similarity index 100% rename from lib/src/utils/cli_params.dart rename to lib/src/utils/cli_args.dart From 07106f70d129eebff63936cc18aac10f72bd9318 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 16:34:02 +0200 Subject: [PATCH 44/88] Run tests --- test/integration_test.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 6614b79e..023d7825 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -148,14 +148,11 @@ void main() async { fail('Platform "$platform" missing from tests'); } - // Also ensure that when a map is present we exercise the dart-symbol-map path (non-web). - // Accept either successful summary or a graceful error if the CLI doesn't yet support the command. + // Ensure that when a map is present we exercise the dart-symbol-map path (non-web). if (platform != 'web' && platform != 'web-legacy') { final hasSummary = pluginOutput.any( (e) => e.contains('Dart symbol map upload summary: attempted=')); - final cliUnsupported = pluginOutput.any((e) => - e.contains("error: unrecognized subcommand 'dart-symbol-map'")); - expect(cliUnsupported || hasSummary, isTrue); + expect(hasSummary, isTrue); } }, timeout: Timeout(const Duration(minutes: 5))); } From 3f3a278459947f00f27fdf23734ac6fb1d0bdbc6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 22:18:59 +0200 Subject: [PATCH 45/88] Add marker --- .../symbol_maps/dart_symbol_map_uploader.dart | 112 +++++++++++++++++- lib/src/utils/cli_args.dart | 2 - test/dart_map_uploader_test.dart | 33 ++++-- 3 files changed, 136 insertions(+), 11 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 9bcd9c26..f7058bad 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -34,6 +34,18 @@ class DartSymbolMapUploader { for (final String debugFilePath in debugFilePaths) { attempted++; + final String? debugId = await _fetchDebugId( + processManager: processManager, + cliPath: config.cliPath!, + debugFilePath: debugFilePath, + ); + if (debugId != null && debugId.isNotEmpty) { + await _prependDebugIdMarkerToMapFile(symbolMapPath, debugId); + } else { + Log.warn( + 'Could not resolve debug id for "$debugFilePath". Proceeding without map modification.'); + } + Log.info( "Uploading Dart symbol map '$symbolMapPath' paired with '$debugFilePath'"); @@ -42,7 +54,6 @@ class DartSymbolMapUploader { 'dart-symbol-map', 'upload', ...config.orgProjectArgs(), - ...config.waitArgs(), symbolMapPath, debugFilePath, ]; @@ -101,4 +112,103 @@ class DartSymbolMapUploader { } return exitCode; } + + /// Returns the debug id for the given [debugFilePath] by invoking: + /// sentry-cli debug-files check --json + /// Returns null on failure. + static Future _fetchDebugId({ + required ProcessManager processManager, + required String cliPath, + required String debugFilePath, + }) async { + try { + final Process process = await processManager.start([ + cliPath, + 'debug-files', + 'check', + '--json', + debugFilePath, + ]); + + final StringBuffer stdoutBuffer = StringBuffer(); + final StringBuffer stderrBuffer = StringBuffer(); + + process.stdout.transform(utf8.decoder).listen(stdoutBuffer.write); + process.stderr.transform(utf8.decoder).listen(stderrBuffer.write); + + final int code = await process.exitCode; + if (code != 0) { + Log.warn( + 'Failed to fetch debug id for "$debugFilePath" (exit=$code): ${stderrBuffer.toString().trim()}'); + return null; + } + + final String output = stdoutBuffer.toString().trim(); + if (output.isEmpty) { + Log.warn('Empty output when fetching debug id for "$debugFilePath"'); + return null; + } + + final dynamic decoded = jsonDecode(output); + if (decoded is! Map) { + Log.warn('Unexpected JSON when fetching debug id for "$debugFilePath"'); + return null; + } + + final variants = decoded['variants']; + if (variants is List && variants.isNotEmpty) { + final first = variants.first; + if (first is Map && first['debug_id'] is String) { + return first['debug_id'] as String; + } + } + + Log.warn('No debug id found in variants for "$debugFilePath"'); + return null; + } catch (e) { + Log.warn('Exception while fetching debug id for "$debugFilePath": $e'); + return null; + } + } + + /// Reads the Dart symbol map at [mapPath] and ensures the array starts with + /// ["SENTRY_DEBUG_ID_MARKER", debugId]. If a previous marker is present, it + /// will be replaced. Fails silently (with logs) on IO/JSON errors. + static Future _prependDebugIdMarkerToMapFile( + String mapPath, String debugId) async { + try { + final File file = File(mapPath); + if (!await file.exists()) { + Log.warn( + "Cannot modify Dart symbol map: file does not exist at '$mapPath'."); + return; + } + + final String raw = await file.readAsString(); + final dynamic decoded = jsonDecode(raw); + if (decoded is! List) { + Log.warn( + 'Cannot modify Dart symbol map: top-level JSON is not an array.'); + return; + } + + final List original = List.from(decoded); + List tail; + if (original.isNotEmpty && original.first == 'SENTRY_DEBUG_ID_MARKER') { + tail = original.length > 2 ? original.sublist(2) : []; + } else { + tail = original; + } + + final List updated = [ + 'SENTRY_DEBUG_ID_MARKER', + debugId, + ...tail, + ]; + + await file.writeAsString(jsonEncode(updated)); + } catch (e) { + Log.warn('Failed to modify Dart symbol map before upload: $e'); + } + } } diff --git a/lib/src/utils/cli_args.dart b/lib/src/utils/cli_args.dart index 8bba7700..e83272dd 100644 --- a/lib/src/utils/cli_args.dart +++ b/lib/src/utils/cli_args.dart @@ -12,6 +12,4 @@ extension SentryCliArgs on Configuration { if (org != null) ...['--org', org!], if (project != null) ...['--project', project!], ]; - - List waitArgs() => [if (waitForProcessing) '--wait']; } diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index 2c1d8f42..105c6621 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -40,9 +40,15 @@ void main() { debugFilePaths: debugFiles, ); - expect(pm.commandLog.length, 2); + expect(pm.commandLog.length, 4); expect( pm.commandLog[0], + equals( + 'mock-cli debug-files check --json ${debugFiles[0]}', + ), + ); + expect( + pm.commandLog[1], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' 'dart-symbol-map upload --org my-org --project my-proj --wait ' @@ -50,7 +56,13 @@ void main() { ), ); expect( - pm.commandLog[1], + pm.commandLog[2], + equals( + 'mock-cli debug-files check --json ${debugFiles[1]}', + ), + ); + expect( + pm.commandLog[3], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' 'dart-symbol-map upload --org my-org --project my-proj --wait ' @@ -78,15 +90,20 @@ void main() { debugFilePaths: debugFiles, ); - expect(pm.commandLog.length, 1); + expect(pm.commandLog.length, 2); + expect( + pm.commandLog[0], + equals('mock-cli debug-files check --json ${debugFiles.single}'), + ); expect( - pm.commandLog.single, + pm.commandLog[1], equals('mock-cli dart-symbol-map upload $map ${debugFiles.single}'), ); }); test('propagates non-zero exit codes via ExitError', () async { - pm.exitCodes = [1]; + // First debug-id check succeeds, then the first upload fails. + pm.exitCodes = [0, 1]; final config = Configuration() ..cliPath = 'mock-cli' @@ -104,9 +121,9 @@ void main() { ); await expectLater(call, throwsA(isA())); - // Only the first command should have been issued because the first - // invocation fails and throws. - expect(pm.commandLog.length, 1); + // Only the first pair of commands (check + upload) should have been issued + // because the first upload fails and throws. + expect(pm.commandLog.length, 2); }); }); } From b605d703f7bfc1ebfb44b60c03c724ea8c022f1a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 22:29:07 +0200 Subject: [PATCH 46/88] Update --- lib/src/symbol_maps/dart_symbol_map.dart | 1 - .../dart_symbol_map_debug_file_collector.dart | 85 ------------------- .../dart_symbol_map_discovery.dart | 83 ++++++++++++++++++ 3 files changed, 83 insertions(+), 86 deletions(-) delete mode 100644 lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 2c0f1b1b..784f35fb 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -2,7 +2,6 @@ import 'package:file/file.dart'; import '../configuration.dart'; import '../utils/log.dart'; -import 'dart_symbol_map_debug_file_collector.dart'; import 'dart_symbol_map_discovery.dart'; import 'dart_symbol_map_uploader.dart'; diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart deleted file mode 100644 index 26911dee..00000000 --- a/lib/src/symbol_maps/dart_symbol_map_debug_file_collector.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:file/file.dart'; - -import '../configuration.dart'; -import '../utils/flutter_debug_files.dart'; - -/// Collects Flutter-relevant native debug file paths that should be paired -/// with a Dart symbol map for symbolication. -/// -/// Policy: -/// - Android: include Flutter-generated `.symbols` files (e.g., -/// `app.android-arm.symbols`, `app.android-arm64.symbols`, `app.android-x64.symbols`). -/// - Apple: include the Mach-O binary `App` inside -/// `App.framework.dSYM/Contents/Resources/DWARF/App`. -/// -/// The function returns absolute, deduplicated paths. It enumerates the -/// configured `symbolsFolder`, `buildFilesFolder`, and other Flutter -/// search roots discovered by `enumerateDebugSearchRoots`. -Future> collectDebugFilesForDartMap({ - required FileSystem fs, - required Configuration config, -}) async { - final Set foundPaths = {}; - - Future collectAndroidSymbolsUnder(String rootPath) async { - if (rootPath.isEmpty) return; - - final Directory directory = fs.directory(rootPath); - if (!await directory.exists()) return; - - await for (final FileSystemEntity entity - in directory.list(recursive: true, followLinks: false)) { - if (entity is! File) continue; - final String basename = fs.path.basename(entity.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin')) { - foundPaths.add(fs.file(entity.path).absolute.path); - } - } - } - - Future collectAppleMachOUnder(String rootPath) async { - if (rootPath.isEmpty) return; - final Directory dir = fs.directory(rootPath); - if (!await dir.exists()) return; - - await for (final FileSystemEntity entity - in dir.list(recursive: true, followLinks: false)) { - if (entity is! Directory) continue; - final String basename = fs.path.basename(entity.path); - if (basename == 'App.framework.dSYM') { - final String machOPath = fs.path.join( - entity.path, - 'Contents', - 'Resources', - 'DWARF', - 'App', - ); - final File machOFile = fs.file(machOPath); - if (await machOFile.exists()) { - foundPaths.add(machOFile.absolute.path); - } - } - } - } - - if (config.symbolsFolder.isNotEmpty) { - await collectAndroidSymbolsUnder(config.symbolsFolder); - } - - if (config.buildFilesFolder != config.symbolsFolder) { - await collectAndroidSymbolsUnder(config.buildFilesFolder); - } - - await collectAppleMachOUnder(config.buildFilesFolder); - - // Enumerate additional Flutter-related roots. - await for (final String root - in enumerateDebugSearchRoots(fs: fs, config: config)) { - await collectAppleMachOUnder(root); - await collectAndroidSymbolsUnder(root); - } - - return foundPaths; -} diff --git a/lib/src/symbol_maps/dart_symbol_map_discovery.dart b/lib/src/symbol_maps/dart_symbol_map_discovery.dart index fabe94e2..ad83280e 100644 --- a/lib/src/symbol_maps/dart_symbol_map_discovery.dart +++ b/lib/src/symbol_maps/dart_symbol_map_discovery.dart @@ -1,4 +1,5 @@ import 'package:file/file.dart'; +import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; import '../configuration.dart'; import '../utils/log.dart'; @@ -29,3 +30,85 @@ Future resolveDartMapPath({ return file.absolute.path; } + +/// Collects Flutter-relevant native debug file paths that should be paired +/// with a Dart symbol map for symbolication. +/// +/// Policy: +/// - Android: include Flutter-generated `.symbols` files (e.g., +/// `app.android-arm.symbols`, `app.android-arm64.symbols`, `app.android-x64.symbols`). +/// - Apple: include the Mach-O binary `App` inside +/// `App.framework.dSYM/Contents/Resources/DWARF/App`. +/// +/// The function returns absolute, deduplicated paths. It enumerates the +/// configured `symbolsFolder`, `buildFilesFolder`, and other Flutter +/// search roots discovered by `enumerateDebugSearchRoots`. +Future> collectDebugFilesForDartMap({ + required FileSystem fs, + required Configuration config, +}) async { + final Set foundPaths = {}; + + Future collectAndroidSymbolsUnder(String rootPath) async { + if (rootPath.isEmpty) return; + + final Directory directory = fs.directory(rootPath); + if (!await directory.exists()) return; + + await for (final FileSystemEntity entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin') && + !basename.contains('ios')) { + foundPaths.add(fs.file(entity.path).absolute.path); + } + } + } + + Future collectAppleMachOUnder(String rootPath) async { + if (rootPath.isEmpty) return; + final Directory dir = fs.directory(rootPath); + if (!await dir.exists()) return; + + await for (final FileSystemEntity entity + in dir.list(recursive: true, followLinks: false)) { + if (entity is! Directory) continue; + final String basename = fs.path.basename(entity.path); + if (basename == 'App.framework.dSYM') { + final String machOPath = fs.path.join( + entity.path, + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } + } + } + + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); + } + + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); + } + + await collectAppleMachOUnder(config.buildFilesFolder); + + // Enumerate additional Flutter-related roots. + await for (final String root + in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAppleMachOUnder(root); + await collectAndroidSymbolsUnder(root); + } + + return foundPaths; +} From ced1eee38f32bd8581f40facff8df8f02a1f1d4b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 22:31:53 +0200 Subject: [PATCH 47/88] Update --- test/dart_map_debug_files_collector_test.dart | 2 +- test/flutter_debug_files_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dart_map_debug_files_collector_test.dart b/test/dart_map_debug_files_collector_test.dart index cf684089..57be02cf 100644 --- a/test/dart_map_debug_files_collector_test.dart +++ b/test/dart_map_debug_files_collector_test.dart @@ -1,8 +1,8 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; void main() { group('collectDebugFilesForDartMap', () { diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart index cf684089..57be02cf 100644 --- a/test/flutter_debug_files_test.dart +++ b/test/flutter_debug_files_test.dart @@ -1,8 +1,8 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; void main() { group('collectDebugFilesForDartMap', () { From 897756c211ff1ed64afe13d6ed698d7283155a3f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 23:50:33 +0200 Subject: [PATCH 48/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index f7058bad..881a76a4 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -114,7 +114,7 @@ class DartSymbolMapUploader { } /// Returns the debug id for the given [debugFilePath] by invoking: - /// sentry-cli debug-files check --json + /// sentry-cli debug-files check --json /debug_file_path /// Returns null on failure. static Future _fetchDebugId({ required ProcessManager processManager, From 1cb2765000e1ec1524497cb642dd3c83bed82763 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 03:16:41 +0200 Subject: [PATCH 49/88] Update --- test/dart_map_uploader_test.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index 105c6621..f8c5fd6e 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -25,8 +25,7 @@ void main() { ..authToken = 'token' ..logLevel = 'debug' ..org = 'my-org' - ..project = 'my-proj' - ..waitForProcessing = true; + ..project = 'my-proj'; final map = '/abs/path/obfuscation.map'; final debugFiles = [ @@ -51,7 +50,7 @@ void main() { pm.commandLog[1], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj --wait ' + 'dart-symbol-map upload --org my-org --project my-proj' '$map ${debugFiles[0]}', ), ); @@ -65,7 +64,7 @@ void main() { pm.commandLog[3], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj --wait ' + 'dart-symbol-map upload --org my-org --project my-proj' '$map ${debugFiles[1]}', ), ); @@ -74,7 +73,6 @@ void main() { test('omits optional flags when not configured', () async { final config = Configuration() ..cliPath = 'mock-cli' - ..waitForProcessing = false ..url = null ..authToken = null ..logLevel = null @@ -111,8 +109,7 @@ void main() { ..project = 'p' ..url = null ..authToken = null - ..logLevel = null - ..waitForProcessing = false; + ..logLevel = null; final call = DartSymbolMapUploader.upload( config: config, From 725ea42a517b48fb9ad7b0a3254ee346e1ca9785 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:04:32 +0200 Subject: [PATCH 50/88] Update --- lib/sentry_dart_plugin.dart | 26 -------------------------- test/dart_map_uploader_test.dart | 4 ++-- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index f1018b57..de96247f 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -17,27 +17,6 @@ import 'src/utils/extensions.dart'; class SentryDartPlugin { late Configuration _configuration; final symbolFileRegexp = RegExp(r'[/\\]app[^/\\]+.*\.(dSYM|symbols)$'); - // Temporary feature flag: guarded no-op until sentry-cli supports Dart symbol map upload. - // Temporary internal gate: keep disabled by default in production. - // Enable in tests when running with the mock CLI or via internal env var. - bool get _dartSymbolMapUploadEnabled { - // Internal escape hatch for CI/tests: set to 'true', '1', or 'yes'. - const String envKey = 'SENTRY_ENABLE_DART_SYMBOL_MAP_UPLOAD'; - final String? envValue = Platform.environment[envKey]?.toLowerCase(); - final bool enabledByEnv = - envValue == 'true' || envValue == '1' || envValue == 'yes'; - - // Auto-enable when tests use the mock CLI. - final String? cliPath = _configuration.cliPath; - final String cliBasename = cliPath == null - ? '' - : cliPath.split(Platform.pathSeparator).isNotEmpty - ? cliPath.split(Platform.pathSeparator).last - : cliPath; - final bool enabledByMockCli = cliBasename == 'mock-cli'; - - return enabledByEnv || enabledByMockCli; - } /// SentryDartPlugin ctor. that inits the injectors SentryDartPlugin() { @@ -173,11 +152,6 @@ class SentryDartPlugin { /// Guarded implementation for uploading Dart symbol map alongside each relevant debug file. /// Currently a no-op until `_dartSymbolMapUploadEnabled` is flipped to true. Future _tryUploadDartSymbolMap() async { - if (!_dartSymbolMapUploadEnabled) { - Log.info('Dart symbol map upload is disabled in this version. Skipping.'); - return; - } - const taskName = 'uploading Dart symbol map(s)'; Log.startingTask(taskName); diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index f8c5fd6e..50a92c14 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -50,7 +50,7 @@ void main() { pm.commandLog[1], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj' + 'dart-symbol-map upload --org my-org --project my-proj ' '$map ${debugFiles[0]}', ), ); @@ -64,7 +64,7 @@ void main() { pm.commandLog[3], equals( 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj' + 'dart-symbol-map upload --org my-org --project my-proj ' '$map ${debugFiles[1]}', ), ); From 78faf96df03e62fabbd8fdf7781f37f448d07241 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:14:23 +0200 Subject: [PATCH 51/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 881a76a4..db809aaa 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -18,6 +18,13 @@ import '../utils/log.dart'; class DartSymbolMapUploader { /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. /// + /// Before uploading the map, we additionally fetch the debug id for the + /// debug file and prepend it to the map. This is necessary because Sentry + /// will reject maps with equivalent content. We artificially add a marker + /// to the map to avoid this. See [_prependDebugIdMarkerToMapFile] for more + /// details. When this is improved in Sentry we can remove the marker and + /// just upload the map. + /// /// Throws [ExitError] on the first non-zero CLI exit code. static Future upload({ required Configuration config, @@ -81,7 +88,7 @@ class DartSymbolMapUploader { } /// Starts the process and forwards stdout/stderr to [Log]. Returns exit code. - /// TODO(buenaflor): eventually this should be deduplicated with the one in sentry_dart_plugin.dart + /// TODO(buenaflor): write a utility function for reuse in other parts of the code. static Future _startAndForward({ required ProcessManager processManager, required String cliPath, @@ -173,7 +180,7 @@ class DartSymbolMapUploader { /// Reads the Dart symbol map at [mapPath] and ensures the array starts with /// ["SENTRY_DEBUG_ID_MARKER", debugId]. If a previous marker is present, it - /// will be replaced. Fails silently (with logs) on IO/JSON errors. + /// will be replaced. static Future _prependDebugIdMarkerToMapFile( String mapPath, String debugId) async { try { From 8a8ee2783c1c5c64bc5dfad1199ebc801c4fb30f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:30:40 +0200 Subject: [PATCH 52/88] Update --- lib/src/symbol_maps/dart_symbol_map.dart | 19 ++++++--- .../dart_symbol_map_discovery.dart | 28 ------------- lib/src/utils/path_utils.dart | 32 +++++++++++++++ test/dart_map_discovery_test.dart | 40 ++++++++++++++++--- 4 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 lib/src/utils/path_utils.dart diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 784f35fb..8ebd241b 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -2,12 +2,15 @@ import 'package:file/file.dart'; import '../configuration.dart'; import '../utils/log.dart'; +import '../utils/path_utils.dart'; import 'dart_symbol_map_discovery.dart'; import 'dart_symbol_map_uploader.dart'; /// Single entrypoint to upload Dart obfuscation map(s) paired with -/// Flutter-relevant native debug files. +/// Flutter-relevant native debug files. This obfuscation map is used to +/// symbolicate Flutter issue titles for non-web platforms. /// +/// Steps: /// - Resolves the Dart symbol map path from config /// - Collects relevant debug files /// - Uploads the map once per debug file via the CLI @@ -15,10 +18,14 @@ Future uploadDartSymbolMaps({ required FileSystem fs, required Configuration config, }) async { - // Validate the configured map path, but pass the original string to the CLI - // to match user-provided (potentially relative) paths expected by tests. - final String? resolvedMapPath = - await resolveDartMapPath(fs: fs, config: config); + final String? resolvedMapPath = await resolveFilePath( + fs: fs, + rawPath: config.dartSymbolMapPath, + missingWarning: + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + notFoundWarningBuilder: (raw) => + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", + ); if (resolvedMapPath == null) { return; } @@ -34,7 +41,7 @@ Future uploadDartSymbolMaps({ await DartSymbolMapUploader.upload( config: config, - symbolMapPath: (config.dartSymbolMapPath ?? '').trim(), + symbolMapPath: resolvedMapPath, debugFilePaths: debugFiles, ); } diff --git a/lib/src/symbol_maps/dart_symbol_map_discovery.dart b/lib/src/symbol_maps/dart_symbol_map_discovery.dart index ad83280e..3eaf14cc 100644 --- a/lib/src/symbol_maps/dart_symbol_map_discovery.dart +++ b/lib/src/symbol_maps/dart_symbol_map_discovery.dart @@ -2,34 +2,6 @@ import 'package:file/file.dart'; import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; import '../configuration.dart'; -import '../utils/log.dart'; - -/// Resolves an absolute path to the Dart obfuscation map file if provided. -/// -/// Behavior: -/// - When `config.dartSymbolMapPath` is null or empty, logs a warning and returns null. -/// - When the provided path does not exist, logs a warning and returns null. -/// - Otherwise, returns the absolute path to the map file. -Future resolveDartMapPath({ - required FileSystem fs, - required Configuration config, -}) async { - final String? providedPath = config.dartSymbolMapPath?.trim(); - if (providedPath == null || providedPath.isEmpty) { - Log.warn( - "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided."); - return null; - } - - final File file = fs.file(providedPath); - if (!await file.exists()) { - Log.warn( - "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'."); - return null; - } - - return file.absolute.path; -} /// Collects Flutter-relevant native debug file paths that should be paired /// with a Dart symbol map for symbolication. diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart new file mode 100644 index 00000000..7a15c7c9 --- /dev/null +++ b/lib/src/utils/path_utils.dart @@ -0,0 +1,32 @@ +import 'package:file/file.dart'; + +import 'log.dart'; + +/// Resolves a config-provided file path to an absolute path if the file exists. +/// +/// - If [rawPath] is null or empty (after trimming), logs [missingWarning] +/// and returns null. +/// - If the file at the trimmed path does not exist, logs the message returned +/// by [notFoundWarningBuilder] (called with the untrimmed [rawPath]) and +/// returns null. +/// - Otherwise returns the absolute path of the file. +Future resolveFilePath({ + required FileSystem fs, + required String? rawPath, + required String missingWarning, + required String Function(String rawPath) notFoundWarningBuilder, +}) async { + final String? providedPath = rawPath?.trim(); + if (providedPath == null || providedPath.isEmpty) { + Log.warn(missingWarning); + return null; + } + + final File file = fs.file(providedPath); + if (!await file.exists()) { + Log.warn(notFoundWarningBuilder(rawPath ?? '')); + return null; + } + + return file.absolute.path; +} diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart index 80d1f256..2814a0dc 100644 --- a/test/dart_map_discovery_test.dart +++ b/test/dart_map_discovery_test.dart @@ -2,10 +2,10 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; +import 'package:sentry_dart_plugin/src/utils/path_utils.dart'; void main() { - group('resolveDartMapPath', () { + group('resolveFilePath for dartSymbolMapPath', () { test('returns absolute path for absolute input', () async { final fs = MemoryFileSystem(style: FileSystemStyle.posix); final projectRoot = fs.directory('/proj')..createSync(recursive: true); @@ -16,7 +16,14 @@ void main() { final config = Configuration()..dartSymbolMapPath = absolutePath; - final result = await resolveDartMapPath(fs: fs, config: config); + final result = await resolveFilePath( + fs: fs, + rawPath: config.dartSymbolMapPath, + missingWarning: + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + notFoundWarningBuilder: (raw) => + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", + ); expect(result, equals(absolutePath)); }); @@ -34,7 +41,14 @@ void main() { ..symbolsFolder = '/root/symbols' ..dartSymbolMapPath = rel; - final result = await resolveDartMapPath(fs: fs, config: config); + final result = await resolveFilePath( + fs: fs, + rawPath: config.dartSymbolMapPath, + missingWarning: + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + notFoundWarningBuilder: (raw) => + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", + ); expect(result, equals(abs)); }); @@ -45,7 +59,14 @@ void main() { final config = Configuration()..dartSymbolMapPath = null; - final result = await resolveDartMapPath(fs: fs, config: config); + final result = await resolveFilePath( + fs: fs, + rawPath: config.dartSymbolMapPath, + missingWarning: + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + notFoundWarningBuilder: (raw) => + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", + ); expect(result, isNull); }); @@ -56,7 +77,14 @@ void main() { final config = Configuration()..dartSymbolMapPath = 'missing.map'; - final result = await resolveDartMapPath(fs: fs, config: config); + final result = await resolveFilePath( + fs: fs, + rawPath: config.dartSymbolMapPath, + missingWarning: + "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", + notFoundWarningBuilder: (raw) => + "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", + ); expect(result, isNull); }); }); From 440caf0e1640da12aeb1bdb45542ceccc37cdaf0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:31:53 +0200 Subject: [PATCH 53/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index db809aaa..a8de6b5b 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -15,6 +15,7 @@ import '../utils/log.dart'; /// sentry-cli dart-symbol-map upload [--url ...] [--auth-token ...] /// [--log-level ...] --org ... --project ... [--wait] /// /path-to-map /path-to-debug-file +/// class DartSymbolMapUploader { /// Uploads [symbolMapPath] for each entry in [debugFilePaths]. /// From 6d69035fb4ed58430e8d7075ffe924b28890255e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:34:46 +0200 Subject: [PATCH 54/88] Update --- lib/src/symbol_maps/dart_symbol_map.dart | 4 ++-- lib/src/utils/path_utils.dart | 15 ++++----------- test/dart_map_discovery_test.dart | 8 ++++---- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 8ebd241b..e0484440 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -21,9 +21,9 @@ Future uploadDartSymbolMaps({ final String? resolvedMapPath = await resolveFilePath( fs: fs, rawPath: config.dartSymbolMapPath, - missingWarning: + missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - notFoundWarningBuilder: (raw) => + fileNotFoundWarning: "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", ); if (resolvedMapPath == null) { diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart index 7a15c7c9..fe9acf79 100644 --- a/lib/src/utils/path_utils.dart +++ b/lib/src/utils/path_utils.dart @@ -3,28 +3,21 @@ import 'package:file/file.dart'; import 'log.dart'; /// Resolves a config-provided file path to an absolute path if the file exists. -/// -/// - If [rawPath] is null or empty (after trimming), logs [missingWarning] -/// and returns null. -/// - If the file at the trimmed path does not exist, logs the message returned -/// by [notFoundWarningBuilder] (called with the untrimmed [rawPath]) and -/// returns null. -/// - Otherwise returns the absolute path of the file. Future resolveFilePath({ required FileSystem fs, required String? rawPath, - required String missingWarning, - required String Function(String rawPath) notFoundWarningBuilder, + required String missingPathWarning, + required String fileNotFoundWarning, }) async { final String? providedPath = rawPath?.trim(); if (providedPath == null || providedPath.isEmpty) { - Log.warn(missingWarning); + Log.warn(missingPathWarning); return null; } final File file = fs.file(providedPath); if (!await file.exists()) { - Log.warn(notFoundWarningBuilder(rawPath ?? '')); + Log.warn(fileNotFoundWarning); return null; } diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart index 2814a0dc..9900d674 100644 --- a/test/dart_map_discovery_test.dart +++ b/test/dart_map_discovery_test.dart @@ -19,7 +19,7 @@ void main() { final result = await resolveFilePath( fs: fs, rawPath: config.dartSymbolMapPath, - missingWarning: + missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", notFoundWarningBuilder: (raw) => "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", @@ -44,7 +44,7 @@ void main() { final result = await resolveFilePath( fs: fs, rawPath: config.dartSymbolMapPath, - missingWarning: + missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", notFoundWarningBuilder: (raw) => "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", @@ -62,7 +62,7 @@ void main() { final result = await resolveFilePath( fs: fs, rawPath: config.dartSymbolMapPath, - missingWarning: + missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", notFoundWarningBuilder: (raw) => "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", @@ -80,7 +80,7 @@ void main() { final result = await resolveFilePath( fs: fs, rawPath: config.dartSymbolMapPath, - missingWarning: + missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", notFoundWarningBuilder: (raw) => "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", From 6b6eff0363340c93cb0dc67b14ece107fd1745f0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:37:10 +0200 Subject: [PATCH 55/88] Update --- lib/sentry_dart_plugin.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index de96247f..523e47d1 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:io'; import 'package:file/file.dart'; import 'package:process/process.dart'; From 67a0f17f8209ea56d638041fcf2a9976a9a73e1e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:37:39 +0200 Subject: [PATCH 56/88] Update --- test/dart_map_discovery_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart index 9900d674..2bca5b22 100644 --- a/test/dart_map_discovery_test.dart +++ b/test/dart_map_discovery_test.dart @@ -21,7 +21,7 @@ void main() { rawPath: config.dartSymbolMapPath, missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - notFoundWarningBuilder: (raw) => + fileNotFoundWarning: "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", ); expect(result, equals(absolutePath)); @@ -46,7 +46,7 @@ void main() { rawPath: config.dartSymbolMapPath, missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - notFoundWarningBuilder: (raw) => + fileNotFoundWarning: "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", ); expect(result, equals(abs)); @@ -64,7 +64,7 @@ void main() { rawPath: config.dartSymbolMapPath, missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - notFoundWarningBuilder: (raw) => + fileNotFoundWarning: "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", ); expect(result, isNull); @@ -82,7 +82,7 @@ void main() { rawPath: config.dartSymbolMapPath, missingPathWarning: "Skipping Dart symbol map uploads: no 'dart_symbol_map_path' provided.", - notFoundWarningBuilder: (raw) => + fileNotFoundWarning: "Skipping Dart symbol map uploads: Dart symbol map file not found at '${config.dartSymbolMapPath}'.", ); expect(result, isNull); From a2bf91cb49e5549fc94c5b47a15a6cbae742883e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:39:09 +0200 Subject: [PATCH 57/88] Update --- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- lib/src/utils/{path_utils.dart => path.dart} | 0 test/dart_map_discovery_test.dart | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/src/utils/{path_utils.dart => path.dart} (100%) diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index e0484440..b5965ea6 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -2,7 +2,7 @@ import 'package:file/file.dart'; import '../configuration.dart'; import '../utils/log.dart'; -import '../utils/path_utils.dart'; +import '../utils/path.dart'; import 'dart_symbol_map_discovery.dart'; import 'dart_symbol_map_uploader.dart'; diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path.dart similarity index 100% rename from lib/src/utils/path_utils.dart rename to lib/src/utils/path.dart diff --git a/test/dart_map_discovery_test.dart b/test/dart_map_discovery_test.dart index 2bca5b22..6ab932d4 100644 --- a/test/dart_map_discovery_test.dart +++ b/test/dart_map_discovery_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/utils/path_utils.dart'; +import 'package:sentry_dart_plugin/src/utils/path.dart'; void main() { group('resolveFilePath for dartSymbolMapPath', () { From 90bbaf0c131891064bc0de018540464dc87b979f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:50:56 +0200 Subject: [PATCH 58/88] Update --- lib/src/configuration_values.dart | 5 -- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- ... dart_symbol_map_collect_debug_files.dart} | 51 ++++++++++++++++--- .../symbol_maps/dart_symbol_map_uploader.dart | 2 +- .../{cli_args.dart => sentry_cli_args.dart} | 0 5 files changed, 47 insertions(+), 13 deletions(-) rename lib/src/symbol_maps/{dart_symbol_map_discovery.dart => dart_symbol_map_collect_debug_files.dart} (58%) rename lib/src/utils/{cli_args.dart => sentry_cli_args.dart} (100%) diff --git a/lib/src/configuration_values.dart b/lib/src/configuration_values.dart index 68710c37..a0d552d1 100644 --- a/lib/src/configuration_values.dart +++ b/lib/src/configuration_values.dart @@ -165,15 +165,10 @@ class ConfigurationValues { if (envSentryCliCdnUrl?.isEmpty ?? false) { envSentryCliCdnUrl = null; } - String? envDartSymbolMapPath = environment['SENTRY_DART_SYMBOL_MAP_PATH']; - if (envDartSymbolMapPath?.isEmpty ?? false) { - envDartSymbolMapPath = null; - } return ConfigurationValues( release: envRelease, dist: envDist, sentryCliCdnUrl: envSentryCliCdnUrl, - dartSymbolMapPath: envDartSymbolMapPath, ); } diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index b5965ea6..74429ad0 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -3,7 +3,7 @@ import 'package:file/file.dart'; import '../configuration.dart'; import '../utils/log.dart'; import '../utils/path.dart'; -import 'dart_symbol_map_discovery.dart'; +import 'dart_symbol_map_collect_debug_files.dart'; import 'dart_symbol_map_uploader.dart'; /// Single entrypoint to upload Dart obfuscation map(s) paired with diff --git a/lib/src/symbol_maps/dart_symbol_map_discovery.dart b/lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart similarity index 58% rename from lib/src/symbol_maps/dart_symbol_map_discovery.dart rename to lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart index 3eaf14cc..f67e217e 100644 --- a/lib/src/symbol_maps/dart_symbol_map_discovery.dart +++ b/lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart @@ -20,6 +20,7 @@ Future> collectDebugFilesForDartMap({ required Configuration config, }) async { final Set foundPaths = {}; + final path = fs.path; Future collectAndroidSymbolsUnder(String rootPath) async { if (rootPath.isEmpty) return; @@ -40,6 +41,25 @@ Future> collectDebugFilesForDartMap({ } } + Future containsAndroidSymbols(String rootPath) async { + if (rootPath.isEmpty) return false; + final Directory directory = fs.directory(rootPath); + if (!await directory.exists()) return false; + + await for (final FileSystemEntity entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin') && + !basename.contains('ios')) { + return true; + } + } + return false; + } + Future collectAppleMachOUnder(String rootPath) async { if (rootPath.isEmpty) return; final Directory dir = fs.directory(rootPath); @@ -65,21 +85,40 @@ Future> collectDebugFilesForDartMap({ } } - if (config.symbolsFolder.isNotEmpty) { - await collectAndroidSymbolsUnder(config.symbolsFolder); + // Prefer scanning Android symbols only under the configured symbols folder. + final List androidRoots = []; + if (config.symbolsFolder.isNotEmpty && + await containsAndroidSymbols(config.symbolsFolder)) { + androidRoots.add(path.normalize(config.symbolsFolder)); + } else if (config.buildFilesFolder.isNotEmpty) { + // Fallback if symbolsFolder is not provided or does not contain any symbols. + androidRoots.add(path.normalize(config.buildFilesFolder)); } - if (config.buildFilesFolder != config.symbolsFolder) { - await collectAndroidSymbolsUnder(config.buildFilesFolder); + for (final String root in androidRoots) { + await collectAndroidSymbolsUnder(root); } + // Always scan the build folder for Apple Mach-O, if present. await collectAppleMachOUnder(config.buildFilesFolder); - // Enumerate additional Flutter-related roots. + // Enumerate additional Flutter-related roots and only consider those + // that are OUTSIDE the build folder to avoid re-traversal. + final List extraRoots = []; await for (final String root in enumerateDebugSearchRoots(fs: fs, config: config)) { + final String normalized = path.normalize(root); + final String buildDir = path.normalize(config.buildFilesFolder); + final bool isInsideBuild = + normalized == buildDir || path.isWithin(buildDir, normalized); + if (!isInsideBuild) { + extraRoots.add(normalized); + } + } + + // Only Apple Mach-O discovery is relevant for these extra roots. + for (final String root in extraRoots) { await collectAppleMachOUnder(root); - await collectAndroidSymbolsUnder(root); } return foundPaths; diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index a8de6b5b..5e2ecf1c 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:process/process.dart'; -import 'package:sentry_dart_plugin/src/utils/cli_args.dart'; +import 'package:sentry_dart_plugin/src/utils/sentry_cli_args.dart'; import '../configuration.dart'; import '../utils/injector.dart'; diff --git a/lib/src/utils/cli_args.dart b/lib/src/utils/sentry_cli_args.dart similarity index 100% rename from lib/src/utils/cli_args.dart rename to lib/src/utils/sentry_cli_args.dart From f4e1ccbf3dab5deb5a52652c18d7c135659428b6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 12:51:11 +0200 Subject: [PATCH 59/88] Update --- lib/sentry_dart_plugin.dart | 1 - test/dart_map_debug_files_collector_test.dart | 2 +- test/flutter_debug_files_test.dart | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 523e47d1..fead503a 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:file/file.dart'; import 'package:process/process.dart'; -// import removed: no longer using directory find extension for symbol discovery import 'src/configuration.dart'; import 'src/utils/flutter_debug_files.dart'; diff --git a/test/dart_map_debug_files_collector_test.dart b/test/dart_map_debug_files_collector_test.dart index 57be02cf..c3dc4c2a 100644 --- a/test/dart_map_debug_files_collector_test.dart +++ b/test/dart_map_debug_files_collector_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_collect_debug_files.dart'; void main() { group('collectDebugFilesForDartMap', () { diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart index 57be02cf..c3dc4c2a 100644 --- a/test/flutter_debug_files_test.dart +++ b/test/flutter_debug_files_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_discovery.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_collect_debug_files.dart'; void main() { group('collectDebugFilesForDartMap', () { From aea323e23c31ef961a1dd92f877b3ac9461aa87e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:07:02 +0200 Subject: [PATCH 60/88] Update --- lib/src/configuration_values.dart | 5 + lib/src/symbol_maps/dart_symbol_map.dart | 2 +- ...art_symbol_map_debug_files_collector.dart} | 56 +----- .../symbol_maps/dart_symbol_map_uploader.dart | 2 +- .../{sentry_cli_args.dart => cli_args.dart} | 0 ...ymbol_map_debug_files_collector_test.dart} | 2 +- ... dart_symbol_map_path_resolving_test.dart} | 0 ...art => dart_symbol_map_uploader_test.dart} | 0 test/flutter_debug_files_test.dart | 159 ------------------ 9 files changed, 16 insertions(+), 210 deletions(-) rename lib/src/symbol_maps/{dart_symbol_map_collect_debug_files.dart => dart_symbol_map_debug_files_collector.dart} (55%) rename lib/src/utils/{sentry_cli_args.dart => cli_args.dart} (100%) rename test/{dart_map_debug_files_collector_test.dart => dart_symbol_map_debug_files_collector_test.dart} (99%) rename test/{dart_map_discovery_test.dart => dart_symbol_map_path_resolving_test.dart} (100%) rename test/{dart_map_uploader_test.dart => dart_symbol_map_uploader_test.dart} (100%) delete mode 100644 test/flutter_debug_files_test.dart diff --git a/lib/src/configuration_values.dart b/lib/src/configuration_values.dart index a0d552d1..68710c37 100644 --- a/lib/src/configuration_values.dart +++ b/lib/src/configuration_values.dart @@ -165,10 +165,15 @@ class ConfigurationValues { if (envSentryCliCdnUrl?.isEmpty ?? false) { envSentryCliCdnUrl = null; } + String? envDartSymbolMapPath = environment['SENTRY_DART_SYMBOL_MAP_PATH']; + if (envDartSymbolMapPath?.isEmpty ?? false) { + envDartSymbolMapPath = null; + } return ConfigurationValues( release: envRelease, dist: envDist, sentryCliCdnUrl: envSentryCliCdnUrl, + dartSymbolMapPath: envDartSymbolMapPath, ); } diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 74429ad0..ba319afc 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -3,7 +3,7 @@ import 'package:file/file.dart'; import '../configuration.dart'; import '../utils/log.dart'; import '../utils/path.dart'; -import 'dart_symbol_map_collect_debug_files.dart'; +import 'dart_symbol_map_debug_files_collector.dart'; import 'dart_symbol_map_uploader.dart'; /// Single entrypoint to upload Dart obfuscation map(s) paired with diff --git a/lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart similarity index 55% rename from lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart rename to lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart index f67e217e..26911dee 100644 --- a/lib/src/symbol_maps/dart_symbol_map_collect_debug_files.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart @@ -1,7 +1,7 @@ import 'package:file/file.dart'; -import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; import '../configuration.dart'; +import '../utils/flutter_debug_files.dart'; /// Collects Flutter-relevant native debug file paths that should be paired /// with a Dart symbol map for symbolication. @@ -20,7 +20,6 @@ Future> collectDebugFilesForDartMap({ required Configuration config, }) async { final Set foundPaths = {}; - final path = fs.path; Future collectAndroidSymbolsUnder(String rootPath) async { if (rootPath.isEmpty) return; @@ -34,32 +33,12 @@ Future> collectDebugFilesForDartMap({ final String basename = fs.path.basename(entity.path); if (basename.startsWith('app') && basename.endsWith('.symbols') && - !basename.contains('darwin') && - !basename.contains('ios')) { + !basename.contains('darwin')) { foundPaths.add(fs.file(entity.path).absolute.path); } } } - Future containsAndroidSymbols(String rootPath) async { - if (rootPath.isEmpty) return false; - final Directory directory = fs.directory(rootPath); - if (!await directory.exists()) return false; - - await for (final FileSystemEntity entity - in directory.list(recursive: true, followLinks: false)) { - if (entity is! File) continue; - final String basename = fs.path.basename(entity.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin') && - !basename.contains('ios')) { - return true; - } - } - return false; - } - Future collectAppleMachOUnder(String rootPath) async { if (rootPath.isEmpty) return; final Directory dir = fs.directory(rootPath); @@ -85,40 +64,21 @@ Future> collectDebugFilesForDartMap({ } } - // Prefer scanning Android symbols only under the configured symbols folder. - final List androidRoots = []; - if (config.symbolsFolder.isNotEmpty && - await containsAndroidSymbols(config.symbolsFolder)) { - androidRoots.add(path.normalize(config.symbolsFolder)); - } else if (config.buildFilesFolder.isNotEmpty) { - // Fallback if symbolsFolder is not provided or does not contain any symbols. - androidRoots.add(path.normalize(config.buildFilesFolder)); + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); } - for (final String root in androidRoots) { - await collectAndroidSymbolsUnder(root); + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); } - // Always scan the build folder for Apple Mach-O, if present. await collectAppleMachOUnder(config.buildFilesFolder); - // Enumerate additional Flutter-related roots and only consider those - // that are OUTSIDE the build folder to avoid re-traversal. - final List extraRoots = []; + // Enumerate additional Flutter-related roots. await for (final String root in enumerateDebugSearchRoots(fs: fs, config: config)) { - final String normalized = path.normalize(root); - final String buildDir = path.normalize(config.buildFilesFolder); - final bool isInsideBuild = - normalized == buildDir || path.isWithin(buildDir, normalized); - if (!isInsideBuild) { - extraRoots.add(normalized); - } - } - - // Only Apple Mach-O discovery is relevant for these extra roots. - for (final String root in extraRoots) { await collectAppleMachOUnder(root); + await collectAndroidSymbolsUnder(root); } return foundPaths; diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 5e2ecf1c..a8de6b5b 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:process/process.dart'; -import 'package:sentry_dart_plugin/src/utils/sentry_cli_args.dart'; +import 'package:sentry_dart_plugin/src/utils/cli_args.dart'; import '../configuration.dart'; import '../utils/injector.dart'; diff --git a/lib/src/utils/sentry_cli_args.dart b/lib/src/utils/cli_args.dart similarity index 100% rename from lib/src/utils/sentry_cli_args.dart rename to lib/src/utils/cli_args.dart diff --git a/test/dart_map_debug_files_collector_test.dart b/test/dart_symbol_map_debug_files_collector_test.dart similarity index 99% rename from test/dart_map_debug_files_collector_test.dart rename to test/dart_symbol_map_debug_files_collector_test.dart index c3dc4c2a..16a60294 100644 --- a/test/dart_map_debug_files_collector_test.dart +++ b/test/dart_symbol_map_debug_files_collector_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_collect_debug_files.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_files_collector.dart'; void main() { group('collectDebugFilesForDartMap', () { diff --git a/test/dart_map_discovery_test.dart b/test/dart_symbol_map_path_resolving_test.dart similarity index 100% rename from test/dart_map_discovery_test.dart rename to test/dart_symbol_map_path_resolving_test.dart diff --git a/test/dart_map_uploader_test.dart b/test/dart_symbol_map_uploader_test.dart similarity index 100% rename from test/dart_map_uploader_test.dart rename to test/dart_symbol_map_uploader_test.dart diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart deleted file mode 100644 index c3dc4c2a..00000000 --- a/test/flutter_debug_files_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:file/memory.dart'; -import 'package:test/test.dart'; - -import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_collect_debug_files.dart'; - -void main() { - group('collectDebugFilesForDartMap', () { - test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', - () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/work')..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/work/build'; - final symbolsDir = '/work/symbols'; - - // Android .symbols files - fs - .file('$symbolsDir/app.android-arm.symbols') - .createSync(recursive: true); - fs - .file('$symbolsDir/app.android-arm64.symbols') - .createSync(recursive: true); - fs - .file('$symbolsDir/app.android-x64.symbols') - .createSync(recursive: true); - - // Apple App.framework.dSYM Mach-O - final appDsymMachO = - '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(appDsymMachO).createSync(recursive: true); - - // Noise: other .dSYM bundles should be ignored - fs - .file( - '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') - .createSync(recursive: true); - fs - .file( - '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') - .createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect( - result, - containsAll([ - fs.path.normalize('/work/symbols/app.android-arm.symbols'), - fs.path.normalize('/work/symbols/app.android-arm64.symbols'), - fs.path.normalize('/work/symbols/app.android-x64.symbols'), - fs.path.normalize(appDsymMachO), - ])); - - // Ensure we did not include non-App.framework dSYMs - expect(result.any((p) => p.endsWith('/Runner')), isFalse); - expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); - - // Ensure deduplication and absoluteness - expect(result.length, 4); - for (final p in result) { - expect(p.startsWith('/'), isTrue, - reason: 'path should be absolute: $p'); - } - }); - - test('finds App.framework.dSYM under Fastlane ios/build path', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/project') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/project/build'; - final symbolsDir = '/project/symbols'; - - // Fastlane path - final machO = - '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(machO).createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, contains(fs.path.normalize(machO))); - }); - - test('finds App.framework.dSYM in macOS build products', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/macosproj') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/macosproj/build'; - final symbolsDir = '/macosproj/symbols'; - - // macOS Products Release path - final macMachO = - '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(macMachO).createSync(recursive: true); - - // Noise: other dSYMs should be ignored - fs - .file( - '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') - .createSync(recursive: true); - fs - .file( - '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') - .createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, contains(fs.path.normalize(macMachO))); - expect(result.any((p) => p.endsWith('/Runner')), isFalse); - expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); - }); - - test('returns empty set when no roots or symbols exist', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/empty') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/empty/build'; - final symbolsDir = '/empty/symbols'; - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, isEmpty); - }); - }); -} From 1dd8c9ca17101d22f193c228a87580b383e4ae7c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:25:42 +0200 Subject: [PATCH 61/88] Update --- test/integration_test.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 023d7825..2b27ecab 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -81,12 +81,7 @@ void main() async { for (var platform in testPlatforms) { test(platform, () async { final appDir = await _prepareTestApp(tempDir, platform); - final pluginOutput = await _runPlugin(appDir, env: { - // Enable dart symbol map upload in integration runs when map path is set. - 'SENTRY_ENABLE_DART_SYMBOL_MAP_UPLOAD': 'true', - // Provide the map path via env to avoid mutating pubspec across cached runs. - 'SENTRY_DART_SYMBOL_MAP_PATH': 'obfuscation.map.json', - }); + final pluginOutput = await _runPlugin(appDir); final serverOutput = await stopServer(); final debugSymbols = uploadedDebugSymbols(serverOutput).keys; @@ -274,6 +269,7 @@ sentry: log_level: debug commits: false legacy_web_symbolication: ${isWebLegacy ? true : false} + dart_symbol_map_path: obfuscation.map.json '''; await pubspecFile.writeAsString(pubspec); From 40ddd0083aef75b32b50fb52bdab1ca3bee33f86 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:26:29 +0200 Subject: [PATCH 62/88] Update --- test/integration_test.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 2b27ecab..a2dbf7e2 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -158,7 +158,7 @@ void main() async { /// /// Returns [_CommandResult] with exitCode and stdout as a single sting Future> _exec(String executable, List arguments, - {String? cwd, Map? environment}) async { + {String? cwd}) async { print( 'executing "$executable ${arguments.join(' ')}"${cwd != null ? ' in $cwd' : ''}'); final process = await Process.start( @@ -166,13 +166,6 @@ Future> _exec(String executable, List arguments, arguments, workingDirectory: cwd, runInShell: true, - // Merge parent env with overrides to allow injecting feature flags in tests. - environment: environment == null - ? null - : { - ...Platform.environment, - ...environment, - }, ); final collector = _ProcessStreamCollector(process); @@ -190,13 +183,10 @@ Future> _exec(String executable, List arguments, Future> _flutter(List arguments, {String? cwd}) => _exec('flutter', arguments, cwd: cwd); -Future> _runPlugin(Directory cwd, - {Map? env}) => - _exec( +Future> _runPlugin(Directory cwd) => _exec( 'dart', ['run', 'sentry_dart_plugin', '--sentry-define=url=$serverUri'], cwd: cwd.path, - environment: env, ); // e.g. Flutter 3.24.4 • channel stable • https://github.com/flutter/flutter.git From 82c629488aae9e6f85f8a6377005a468db1b251a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:32:25 +0200 Subject: [PATCH 63/88] Update CHANGELOG --- CHANGELOG.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 746ad109..4f71400a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,11 @@ ### Features -- Upload dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) - - Allows Flutter issue title symbolication - -### Dependencies - -- Bump CLI from v2.41.1 to v2.52.0 ([#327](https://github.com/getsentry/sentry-dart-plugin/pull/327)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2520) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.41.1...2.52.0) +- Upload Dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) + - What it does: Enables symbolication of Flutter issue titles for obfuscated Android, iOS, and macOS builds. Linux and Windows are not supported yet. + - Generate the mapping file: Add `--extra-gen-snapshot-options=--save-obfuscation-map=` when building. Example: `flutter build apk --obfuscate --split-debug-info=build/symbols --extra-gen-snapshot-options=--save-obfuscation-map=build/mapping.json` + - Configure the plugin: Set `dart_symbol_map_path: build/mapping.json` + - Important: `dart_symbol_map_path` must point directly to the mapping file (absolute or relative path), not a directory. ## 3.1.1 From 2b63ca8a18ca56e0ff5986d65ebd529a477552b6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:33:35 +0200 Subject: [PATCH 64/88] Update CHANGELOG --- CHANGELOG.md | 78 +++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f71400a..f46674fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,13 @@ ### Features +### Features + - Upload Dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) - - What it does: Enables symbolication of Flutter issue titles for obfuscated Android, iOS, and macOS builds. Linux and Windows are not supported yet. - - Generate the mapping file: Add `--extra-gen-snapshot-options=--save-obfuscation-map=` when building. Example: `flutter build apk --obfuscate --split-debug-info=build/symbols --extra-gen-snapshot-options=--save-obfuscation-map=build/mapping.json` - - Configure the plugin: Set `dart_symbol_map_path: build/mapping.json` - - Important: `dart_symbol_map_path` must point directly to the mapping file (absolute or relative path), not a directory. + - What it does: Enables symbolication of Flutter issue titles for obfuscated Android, iOS, and macOS builds. Linux and Windows are not supported yet. + - Generate the mapping file: Add `--extra-gen-snapshot-options=--save-obfuscation-map=` when building. Example: `flutter build apk --obfuscate --split-debug-info=build/symbols --extra-gen-snapshot-options=--save-obfuscation-map=build/mapping.json` + - Configure the plugin: Set `dart_symbol_map_path: build/mapping.json` + - Important: `dart_symbol_map_path` must point directly to the mapping file (absolute or relative path), not a directory. ## 3.1.1 @@ -41,12 +43,12 @@ Version 3.0.0 marks a major release of the Sentry Dart Plugin containing breakin - If you’re on **9.0.0** (or below), you **won’t** get Debug IDs automatically. 3. **Legacy Symbolication Mode** - If you **cannot upgrade** to Flutter SDK ≥ 9.1.0 **yet**, add this flag to your Sentry Dart Plugin config: - ```yaml - sentry: - dart_plugin: - legacy_web_symbolication: true - ``` - - This switches back to the “classic” source-map symbolication method you’ve been using. + ```yaml + sentry: + dart_plugin: + legacy_web_symbolication: true + ``` + * This switches back to the “classic” source-map symbolication method you’ve been using. ### Features @@ -248,97 +250,97 @@ Version 3.0.0 marks a major release of the Sentry Dart Plugin containing breakin ### Dependencies -- Bump CLI from v2.13.0 to v2.17.5 ([#86](https://github.com/getsentry/sentry-dart-plugin/pull/86), [#89](https://github.com/getsentry/sentry-dart-plugin/pull/89), [#90](https://github.com/getsentry/sentry-dart-plugin/pull/90), [#101](https://github.com/getsentry/sentry-dart-plugin/pull/101), [#103](https://github.com/getsentry/sentry-dart-plugin/pull/103), [#107](https://github.com/getsentry/sentry-dart-plugin/pull/107), [#114](https://github.com/getsentry/sentry-dart-plugin/pull/114)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2175) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.13.0...2.17.5) +* Bump CLI from v2.13.0 to v2.17.5 ([#86](https://github.com/getsentry/sentry-dart-plugin/pull/86), [#89](https://github.com/getsentry/sentry-dart-plugin/pull/89), [#90](https://github.com/getsentry/sentry-dart-plugin/pull/90), [#101](https://github.com/getsentry/sentry-dart-plugin/pull/101), [#103](https://github.com/getsentry/sentry-dart-plugin/pull/103), [#107](https://github.com/getsentry/sentry-dart-plugin/pull/107), [#114](https://github.com/getsentry/sentry-dart-plugin/pull/114)) + * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2175) + * [diff](https://github.com/getsentry/sentry-cli/compare/2.13.0...2.17.5) ## 1.1.0 ### Features -- Add configuration `ignore_missing` ([#85](https://github.com/getsentry/sentry-dart-plugin/pull/85)) +* Add configuration `ignore_missing` ([#85](https://github.com/getsentry/sentry-dart-plugin/pull/85)) ## 1.0.0 ### Dependencies -- Bump CLI from v2.12.0 to v2.13.0 ([#80](https://github.com/getsentry/sentry-dart-plugin/pull/80)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2130) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.12.0...2.13.0) +* Bump CLI from v2.12.0 to v2.13.0 ([#80](https://github.com/getsentry/sentry-dart-plugin/pull/80)) + * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2130) + * [diff](https://github.com/getsentry/sentry-cli/compare/2.12.0...2.13.0) ## 1.0.0-RC.1 ### Changes -- Rename configuration `include_native_sources` to `upload_sources` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) -- Rename configuration `upload_native_symbols` to `upload_debug_symbols` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) +* Rename configuration `include_native_sources` to `upload_sources` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) +* Rename configuration `upload_native_symbols` to `upload_debug_symbols` ([#78](https://github.com/getsentry/sentry-dart-plugin/pull/78)) ### Dependencies -- Bump CLI from v2.11.0 to v2.12.0 ([#79](https://github.com/getsentry/sentry-dart-plugin/pull/79)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2120) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.11.0...2.12.0) +* Bump CLI from v2.11.0 to v2.12.0 ([#79](https://github.com/getsentry/sentry-dart-plugin/pull/79)) + * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2120) + * [diff](https://github.com/getsentry/sentry-cli/compare/2.11.0...2.12.0) ## 1.0.0-beta.5 ### Dependencies -- Bump CLI from v2.7.0 to v2.11.0 ([#65](https://github.com/getsentry/sentry-dart-plugin/pull/65), [#67](https://github.com/getsentry/sentry-dart-plugin/pull/67), [#69](https://github.com/getsentry/sentry-dart-plugin/pull/69), [#72](https://github.com/getsentry/sentry-dart-plugin/pull/72), [#74](https://github.com/getsentry/sentry-dart-plugin/pull/74)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2110) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.7.0...2.11.0) -- Bump system_info2 to ^3.0.1 ([#77](https://github.com/getsentry/sentry-dart-plugin/pull/77)) +* Bump CLI from v2.7.0 to v2.11.0 ([#65](https://github.com/getsentry/sentry-dart-plugin/pull/65), [#67](https://github.com/getsentry/sentry-dart-plugin/pull/67), [#69](https://github.com/getsentry/sentry-dart-plugin/pull/69), [#72](https://github.com/getsentry/sentry-dart-plugin/pull/72), [#74](https://github.com/getsentry/sentry-dart-plugin/pull/74)) + * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2110) + * [diff](https://github.com/getsentry/sentry-cli/compare/2.7.0...2.11.0) +* Bump system_info2 to ^3.0.1 ([#77](https://github.com/getsentry/sentry-dart-plugin/pull/77)) ## 1.0.0-beta.4 ### Features -- Support release commits ([#62](https://github.com/getsentry/sentry-dart-plugin/pull/62)) +* Support release commits ([#62](https://github.com/getsentry/sentry-dart-plugin/pull/62)) ## 1.0.0-beta.3 ### Features -- Add support to load release variable from environment ([#40](https://github.com/getsentry/sentry-dart-plugin/pull/40)) -- Download Sentry CLI on first run ([#49](https://github.com/getsentry/sentry-dart-plugin/pull/49)) +* Add support to load release variable from environment ([#40](https://github.com/getsentry/sentry-dart-plugin/pull/40)) +* Download Sentry CLI on first run ([#49](https://github.com/getsentry/sentry-dart-plugin/pull/49)) ### Dependencies -- Bump CLI from v2.6.0 to v2.7.0 ([#57](https://github.com/getsentry/sentry-dart-plugin/pull/57)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#270) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.6.0...2.7.0) +* Bump CLI from v2.6.0 to v2.7.0 ([#57](https://github.com/getsentry/sentry-dart-plugin/pull/57)) + * [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#270) + * [diff](https://github.com/getsentry/sentry-cli/compare/2.6.0...2.7.0) ## 1.0.0-beta.2 ### Fixes -- Early exit when providing lower log level ([#31](https://github.com/getsentry/sentry-dart-plugin/pull/31)) +* Early exit when providing lower log level ([#31](https://github.com/getsentry/sentry-dart-plugin/pull/31)) ## 1.0.0-beta.1 ### Features -- Ability to configure url for on-premise server ([#17](https://github.com/getsentry/sentry-dart-plugin/pull/17)) +* Ability to configure url for on-premise server ([#17](https://github.com/getsentry/sentry-dart-plugin/pull/17)) ## 1.0.0-alpha.4 ### Fixes -- Log real exitCode, stdout and stdout if available ([#13](https://github.com/getsentry/sentry-dart-plugin/pull/13)) +* Log real exitCode, stdout and stdout if available ([#13](https://github.com/getsentry/sentry-dart-plugin/pull/13)) ## 1.0.0-alpha.3 ### Dependencies -- Bump sentry-cli 1.69.1 which includes a fix for Dart debug symbols ([#8](https://github.com/getsentry/sentry-dart-plugin/pull/8)) +* Bump sentry-cli 1.69.1 which includes a fix for Dart debug symbols ([#8](https://github.com/getsentry/sentry-dart-plugin/pull/8)) ## 1.0.0-alpha.2 ### Fixes -- Add org and project when creating releases ([#2](https://github.com/getsentry/sentry-dart-plugin/pull/2)) +* Add org and project when creating releases ([#2](https://github.com/getsentry/sentry-dart-plugin/pull/2)) ## 1.0.0-alpha.1 ### Features -- Sentry Dart Plugin +* Sentry Dart Plugin From 089f82e52525bf9ad1720f19281ec8952c7c9aa1 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 13:57:28 +0200 Subject: [PATCH 65/88] Update --- ...dart_symbol_map_debug_files_collector.dart | 56 ++++- lib/src/utils/path_utils.dart | 25 +++ test/dart_map_debug_file_collector_test.dart | 159 ++++++++++++++ test/dart_map_uploader_test.dart | 196 ++++++++++++++++++ test/dart_symbol_map_path_resolving_test.dart | 2 +- 5 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 lib/src/utils/path_utils.dart create mode 100644 test/dart_map_debug_file_collector_test.dart create mode 100644 test/dart_map_uploader_test.dart diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart index 26911dee..f67e217e 100644 --- a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart @@ -1,7 +1,7 @@ import 'package:file/file.dart'; +import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; import '../configuration.dart'; -import '../utils/flutter_debug_files.dart'; /// Collects Flutter-relevant native debug file paths that should be paired /// with a Dart symbol map for symbolication. @@ -20,6 +20,7 @@ Future> collectDebugFilesForDartMap({ required Configuration config, }) async { final Set foundPaths = {}; + final path = fs.path; Future collectAndroidSymbolsUnder(String rootPath) async { if (rootPath.isEmpty) return; @@ -33,12 +34,32 @@ Future> collectDebugFilesForDartMap({ final String basename = fs.path.basename(entity.path); if (basename.startsWith('app') && basename.endsWith('.symbols') && - !basename.contains('darwin')) { + !basename.contains('darwin') && + !basename.contains('ios')) { foundPaths.add(fs.file(entity.path).absolute.path); } } } + Future containsAndroidSymbols(String rootPath) async { + if (rootPath.isEmpty) return false; + final Directory directory = fs.directory(rootPath); + if (!await directory.exists()) return false; + + await for (final FileSystemEntity entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin') && + !basename.contains('ios')) { + return true; + } + } + return false; + } + Future collectAppleMachOUnder(String rootPath) async { if (rootPath.isEmpty) return; final Directory dir = fs.directory(rootPath); @@ -64,21 +85,40 @@ Future> collectDebugFilesForDartMap({ } } - if (config.symbolsFolder.isNotEmpty) { - await collectAndroidSymbolsUnder(config.symbolsFolder); + // Prefer scanning Android symbols only under the configured symbols folder. + final List androidRoots = []; + if (config.symbolsFolder.isNotEmpty && + await containsAndroidSymbols(config.symbolsFolder)) { + androidRoots.add(path.normalize(config.symbolsFolder)); + } else if (config.buildFilesFolder.isNotEmpty) { + // Fallback if symbolsFolder is not provided or does not contain any symbols. + androidRoots.add(path.normalize(config.buildFilesFolder)); } - if (config.buildFilesFolder != config.symbolsFolder) { - await collectAndroidSymbolsUnder(config.buildFilesFolder); + for (final String root in androidRoots) { + await collectAndroidSymbolsUnder(root); } + // Always scan the build folder for Apple Mach-O, if present. await collectAppleMachOUnder(config.buildFilesFolder); - // Enumerate additional Flutter-related roots. + // Enumerate additional Flutter-related roots and only consider those + // that are OUTSIDE the build folder to avoid re-traversal. + final List extraRoots = []; await for (final String root in enumerateDebugSearchRoots(fs: fs, config: config)) { + final String normalized = path.normalize(root); + final String buildDir = path.normalize(config.buildFilesFolder); + final bool isInsideBuild = + normalized == buildDir || path.isWithin(buildDir, normalized); + if (!isInsideBuild) { + extraRoots.add(normalized); + } + } + + // Only Apple Mach-O discovery is relevant for these extra roots. + for (final String root in extraRoots) { await collectAppleMachOUnder(root); - await collectAndroidSymbolsUnder(root); } return foundPaths; diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart new file mode 100644 index 00000000..fe9acf79 --- /dev/null +++ b/lib/src/utils/path_utils.dart @@ -0,0 +1,25 @@ +import 'package:file/file.dart'; + +import 'log.dart'; + +/// Resolves a config-provided file path to an absolute path if the file exists. +Future resolveFilePath({ + required FileSystem fs, + required String? rawPath, + required String missingPathWarning, + required String fileNotFoundWarning, +}) async { + final String? providedPath = rawPath?.trim(); + if (providedPath == null || providedPath.isEmpty) { + Log.warn(missingPathWarning); + return null; + } + + final File file = fs.file(providedPath); + if (!await file.exists()) { + Log.warn(fileNotFoundWarning); + return null; + } + + return file.absolute.path; +} diff --git a/test/dart_map_debug_file_collector_test.dart b/test/dart_map_debug_file_collector_test.dart new file mode 100644 index 00000000..b5a998a4 --- /dev/null +++ b/test/dart_map_debug_file_collector_test.dart @@ -0,0 +1,159 @@ +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; + +void main() { + group('collectDebugFilesForDartMap', () { + test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', + () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/work')..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/work/build'; + final symbolsDir = '/work/symbols'; + + // Android .symbols files + fs + .file('$symbolsDir/app.android-arm.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-arm64.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-x64.symbols') + .createSync(recursive: true); + + // Apple App.framework.dSYM Mach-O + final appDsymMachO = + '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(appDsymMachO).createSync(recursive: true); + + // Noise: other .dSYM bundles should be ignored + fs + .file( + '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect( + result, + containsAll([ + fs.path.normalize('/work/symbols/app.android-arm.symbols'), + fs.path.normalize('/work/symbols/app.android-arm64.symbols'), + fs.path.normalize('/work/symbols/app.android-x64.symbols'), + fs.path.normalize(appDsymMachO), + ])); + + // Ensure we did not include non-App.framework dSYMs + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + + // Ensure deduplication and absoluteness + expect(result.length, 4); + for (final p in result) { + expect(p.startsWith('/'), isTrue, + reason: 'path should be absolute: $p'); + } + }); + + test('finds App.framework.dSYM under Fastlane ios/build path', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/project') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/project/build'; + final symbolsDir = '/project/symbols'; + + // Fastlane path + final machO = + '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(machO).createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(machO))); + }); + + test('finds App.framework.dSYM in macOS build products', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/macosproj') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/macosproj/build'; + final symbolsDir = '/macosproj/symbols'; + + // macOS Products Release path + final macMachO = + '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(macMachO).createSync(recursive: true); + + // Noise: other dSYMs should be ignored + fs + .file( + '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(macMachO))); + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + }); + + test('returns empty set when no roots or symbols exist', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/empty') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/empty/build'; + final symbolsDir = '/empty/symbols'; + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await collectDebugFilesForDartMap( + fs: fs, + config: config, + ); + + expect(result, isEmpty); + }); + }); +} diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart new file mode 100644 index 00000000..50a92c14 --- /dev/null +++ b/test/dart_map_uploader_test.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:process/process.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/configuration.dart'; +import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_uploader.dart'; +import 'package:sentry_dart_plugin/src/utils/injector.dart'; +import 'package:sentry_dart_plugin/src/utils/log.dart'; + +void main() { + group('DartMapUploader.upload', () { + late MockProcessManager pm; + + setUp(() { + pm = MockProcessManager(); + injector.registerSingleton(() => pm, override: true); + }); + + test('emits one command per debug file with all flags', () async { + final config = Configuration() + ..cliPath = 'mock-cli' + ..url = 'https://example.invalid' + ..authToken = 'token' + ..logLevel = 'debug' + ..org = 'my-org' + ..project = 'my-proj'; + + final map = '/abs/path/obfuscation.map'; + final debugFiles = [ + '/a/app.android-arm.symbols', + '/b/App.framework.dSYM/Contents/Resources/DWARF/App', + ]; + + await DartSymbolMapUploader.upload( + config: config, + symbolMapPath: map, + debugFilePaths: debugFiles, + ); + + expect(pm.commandLog.length, 4); + expect( + pm.commandLog[0], + equals( + 'mock-cli debug-files check --json ${debugFiles[0]}', + ), + ); + expect( + pm.commandLog[1], + equals( + 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' + 'dart-symbol-map upload --org my-org --project my-proj ' + '$map ${debugFiles[0]}', + ), + ); + expect( + pm.commandLog[2], + equals( + 'mock-cli debug-files check --json ${debugFiles[1]}', + ), + ); + expect( + pm.commandLog[3], + equals( + 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' + 'dart-symbol-map upload --org my-org --project my-proj ' + '$map ${debugFiles[1]}', + ), + ); + }); + + test('omits optional flags when not configured', () async { + final config = Configuration() + ..cliPath = 'mock-cli' + ..url = null + ..authToken = null + ..logLevel = null + ..org = null + ..project = null; + + final map = '/m/map.json'; + final debugFiles = ['/d/file.symbols']; + + await DartSymbolMapUploader.upload( + config: config, + symbolMapPath: map, + debugFilePaths: debugFiles, + ); + + expect(pm.commandLog.length, 2); + expect( + pm.commandLog[0], + equals('mock-cli debug-files check --json ${debugFiles.single}'), + ); + expect( + pm.commandLog[1], + equals('mock-cli dart-symbol-map upload $map ${debugFiles.single}'), + ); + }); + + test('propagates non-zero exit codes via ExitError', () async { + // First debug-id check succeeds, then the first upload fails. + pm.exitCodes = [0, 1]; + + final config = Configuration() + ..cliPath = 'mock-cli' + ..org = 'o' + ..project = 'p' + ..url = null + ..authToken = null + ..logLevel = null; + + final call = DartSymbolMapUploader.upload( + config: config, + symbolMapPath: '/map.json', + debugFilePaths: ['/debug.symbols', '/ignored.second'], + ); + + await expectLater(call, throwsA(isA())); + // Only the first pair of commands (check + upload) should have been issued + // because the first upload fails and throws. + expect(pm.commandLog.length, 2); + }); + }); +} + +class MockProcessManager implements ProcessManager { + final List commandLog = []; + List exitCodes = []; // optional per-start exit codes + + @override + bool canRun(executable, {String? workingDirectory}) => true; + + @override + bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) => true; + + @override + Future run(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + covariant Encoding? stdoutEncoding = systemEncoding, + covariant Encoding? stderrEncoding = systemEncoding}) async { + return runSync(command); + } + + @override + ProcessResult runSync(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + covariant Encoding? stdoutEncoding = systemEncoding, + covariant Encoding? stderrEncoding = systemEncoding}) { + commandLog.add(command.join(' ')); + final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; + return ProcessResult(-1, code, null, null); + } + + @override + Future start(List command, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + ProcessStartMode mode = ProcessStartMode.normal}) async { + commandLog.add(command.join(' ')); + final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; + return MockProcess(code); + } +} + +class MockProcess implements Process { + final int _exitCode; + MockProcess(this._exitCode); + + @override + Future get exitCode => Future.value(_exitCode); + + @override + bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => false; + + @override + int get pid => -1; + + @override + Stream> get stderr => const Stream>.empty(); + + @override + IOSink get stdin => throw UnimplementedError(); + + @override + Stream> get stdout => const Stream>.empty(); +} diff --git a/test/dart_symbol_map_path_resolving_test.dart b/test/dart_symbol_map_path_resolving_test.dart index 6ab932d4..2bca5b22 100644 --- a/test/dart_symbol_map_path_resolving_test.dart +++ b/test/dart_symbol_map_path_resolving_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/utils/path.dart'; +import 'package:sentry_dart_plugin/src/utils/path_utils.dart'; void main() { group('resolveFilePath for dartSymbolMapPath', () { From 54dd327457b6b0e0dde37e2598a95cb2327689f5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 14:47:26 +0200 Subject: [PATCH 66/88] Update --- ...dart_symbol_map_debug_files_collector.dart | 65 +++---- test/dart_map_debug_file_collector_test.dart | 159 ------------------ 2 files changed, 26 insertions(+), 198 deletions(-) delete mode 100644 test/dart_map_debug_file_collector_test.dart diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart index f67e217e..f8d314dd 100644 --- a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart @@ -60,30 +60,10 @@ Future> collectDebugFilesForDartMap({ return false; } - Future collectAppleMachOUnder(String rootPath) async { - if (rootPath.isEmpty) return; - final Directory dir = fs.directory(rootPath); - if (!await dir.exists()) return; - - await for (final FileSystemEntity entity - in dir.list(recursive: true, followLinks: false)) { - if (entity is! Directory) continue; - final String basename = fs.path.basename(entity.path); - if (basename == 'App.framework.dSYM') { - final String machOPath = fs.path.join( - entity.path, - 'Contents', - 'Resources', - 'DWARF', - 'App', - ); - final File machOFile = fs.file(machOPath); - if (await machOFile.exists()) { - foundPaths.add(machOFile.absolute.path); - } - } - } - } + // No recursive Apple scan needed. We only care about the Mach-O at + // /App.framework.dSYM/Contents/Resources/DWARF/App. For roots that + // end with 'Runner.app', the dSYM lives next to it, so we probe the parent + // directory as the base. // Prefer scanning Android symbols only under the configured symbols folder. final List androidRoots = []; @@ -99,26 +79,33 @@ Future> collectDebugFilesForDartMap({ await collectAndroidSymbolsUnder(root); } - // Always scan the build folder for Apple Mach-O, if present. - await collectAppleMachOUnder(config.buildFilesFolder); - - // Enumerate additional Flutter-related roots and only consider those - // that are OUTSIDE the build folder to avoid re-traversal. - final List extraRoots = []; + // Enumerate Flutter-related roots and only consider those that contain + // 'ios' or 'macos'. Compute the expected Mach-O path directly for each. await for (final String root in enumerateDebugSearchRoots(fs: fs, config: config)) { final String normalized = path.normalize(root); - final String buildDir = path.normalize(config.buildFilesFolder); - final bool isInsideBuild = - normalized == buildDir || path.isWithin(buildDir, normalized); - if (!isInsideBuild) { - extraRoots.add(normalized); + final String lower = normalized.toLowerCase(); + if (!(lower.contains('ios') || lower.contains('macos'))) { + continue; } - } - // Only Apple Mach-O discovery is relevant for these extra roots. - for (final String root in extraRoots) { - await collectAppleMachOUnder(root); + for (final String candidateBase in { + normalized, + path.dirname(normalized), + }) { + final String machOPath = path.join( + candidateBase, + 'App.framework.dSYM', + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } } return foundPaths; diff --git a/test/dart_map_debug_file_collector_test.dart b/test/dart_map_debug_file_collector_test.dart deleted file mode 100644 index b5a998a4..00000000 --- a/test/dart_map_debug_file_collector_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:file/memory.dart'; -import 'package:test/test.dart'; - -import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_debug_file_collector.dart'; - -void main() { - group('collectDebugFilesForDartMap', () { - test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', - () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/work')..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/work/build'; - final symbolsDir = '/work/symbols'; - - // Android .symbols files - fs - .file('$symbolsDir/app.android-arm.symbols') - .createSync(recursive: true); - fs - .file('$symbolsDir/app.android-arm64.symbols') - .createSync(recursive: true); - fs - .file('$symbolsDir/app.android-x64.symbols') - .createSync(recursive: true); - - // Apple App.framework.dSYM Mach-O - final appDsymMachO = - '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(appDsymMachO).createSync(recursive: true); - - // Noise: other .dSYM bundles should be ignored - fs - .file( - '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') - .createSync(recursive: true); - fs - .file( - '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') - .createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect( - result, - containsAll([ - fs.path.normalize('/work/symbols/app.android-arm.symbols'), - fs.path.normalize('/work/symbols/app.android-arm64.symbols'), - fs.path.normalize('/work/symbols/app.android-x64.symbols'), - fs.path.normalize(appDsymMachO), - ])); - - // Ensure we did not include non-App.framework dSYMs - expect(result.any((p) => p.endsWith('/Runner')), isFalse); - expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); - - // Ensure deduplication and absoluteness - expect(result.length, 4); - for (final p in result) { - expect(p.startsWith('/'), isTrue, - reason: 'path should be absolute: $p'); - } - }); - - test('finds App.framework.dSYM under Fastlane ios/build path', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/project') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/project/build'; - final symbolsDir = '/project/symbols'; - - // Fastlane path - final machO = - '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(machO).createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, contains(fs.path.normalize(machO))); - }); - - test('finds App.framework.dSYM in macOS build products', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/macosproj') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/macosproj/build'; - final symbolsDir = '/macosproj/symbols'; - - // macOS Products Release path - final macMachO = - '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(macMachO).createSync(recursive: true); - - // Noise: other dSYMs should be ignored - fs - .file( - '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') - .createSync(recursive: true); - fs - .file( - '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') - .createSync(recursive: true); - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, contains(fs.path.normalize(macMachO))); - expect(result.any((p) => p.endsWith('/Runner')), isFalse); - expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); - }); - - test('returns empty set when no roots or symbols exist', () async { - final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/empty') - ..createSync(recursive: true); - fs.currentDirectory = projectRootDir; - - final buildDir = '/empty/build'; - final symbolsDir = '/empty/symbols'; - - final config = Configuration() - ..buildFilesFolder = buildDir - ..symbolsFolder = symbolsDir; - - final result = await collectDebugFilesForDartMap( - fs: fs, - config: config, - ); - - expect(result, isEmpty); - }); - }); -} From 1af9d5b3dcab7170c61e7f72ea4da2e49bc16564 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:07:40 +0200 Subject: [PATCH 67/88] Update --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- lib/src/utils/path.dart | 2 +- .../utils/{cli_args.dart => sentry_cli_args.dart} | 0 test/plugin_test.dart | 15 +++++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) rename lib/src/utils/{cli_args.dart => sentry_cli_args.dart} (100%) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index a8de6b5b..5e2ecf1c 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:process/process.dart'; -import 'package:sentry_dart_plugin/src/utils/cli_args.dart'; +import 'package:sentry_dart_plugin/src/utils/sentry_cli_args.dart'; import '../configuration.dart'; import '../utils/injector.dart'; diff --git a/lib/src/utils/path.dart b/lib/src/utils/path.dart index fe9acf79..5339bf00 100644 --- a/lib/src/utils/path.dart +++ b/lib/src/utils/path.dart @@ -2,7 +2,7 @@ import 'package:file/file.dart'; import 'log.dart'; -/// Resolves a config-provided file path to an absolute path if the file exists. +/// Resolves a provided file path to an absolute path if the file exists. Future resolveFilePath({ required FileSystem fs, required String? rawPath, diff --git a/lib/src/utils/cli_args.dart b/lib/src/utils/sentry_cli_args.dart similarity index 100% rename from lib/src/utils/cli_args.dart rename to lib/src/utils/sentry_cli_args.dart diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 2944a21d..29076361 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -217,7 +217,7 @@ void main() { }); }); - test('emits dart-symbol-map upload command when enabled', () async { + test('emits dart-symbol-map upload command', () async { const version = '1.0.0'; // Create Android symbols and a fake Dart symbol map final androidSymbolsDir = fs.directory('$buildDir/app/outputs') @@ -238,13 +238,16 @@ void main() { const release = '$name@$version'; final args = '$commonArgs --log-level debug'; - // Accept either relative or absolute path and potential '/./' normalization in the debug file path expect( commandLog, - anyElement((e) => - e.startsWith( - '$cli $args dart-symbol-map upload $orgAndProject ${mapFile.path} ') && - e.endsWith('$buildDir/app/outputs/app-release.symbols')), + anyElement((e) { + final relStart = + '$cli $args dart-symbol-map upload $orgAndProject ${mapFile.path} '; + final absStart = + '$cli $args dart-symbol-map upload $orgAndProject ${fs.file(mapFile.path).absolute.path} '; + return (e.startsWith(relStart) || e.startsWith(absStart)) && + e.endsWith('$buildDir/app/outputs/app-release.symbols'); + }), ); // Ensure other expected commands still present From f0b27b5c7fb7a7b6410e77242934a7140fcfcbea Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:09:42 +0200 Subject: [PATCH 68/88] Update --- test/integration_test.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index a2dbf7e2..34ed3e23 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -143,8 +143,11 @@ void main() async { fail('Platform "$platform" missing from tests'); } - // Ensure that when a map is present we exercise the dart-symbol-map path (non-web). - if (platform != 'web' && platform != 'web-legacy') { + // Ensure that when a map is present we exercise the dart-symbol-map path for supported platforms. + if (platform == 'ios' || + platform == 'ios-framework' || + platform == 'macos' || + platform == 'macos-framework') { final hasSummary = pluginOutput.any( (e) => e.contains('Dart symbol map upload summary: attempted=')); expect(hasSummary, isTrue); From 9a35bd68ba14124731cc07f930a34e025d3f9199 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:09:56 +0200 Subject: [PATCH 69/88] Update --- test/integration_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 34ed3e23..c7ad8f8e 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -147,7 +147,9 @@ void main() async { if (platform == 'ios' || platform == 'ios-framework' || platform == 'macos' || - platform == 'macos-framework') { + platform == 'macos-framework' || + platform == 'apk' || + platform == 'appbundle') { final hasSummary = pluginOutput.any( (e) => e.contains('Dart symbol map upload summary: attempted=')); expect(hasSummary, isTrue); From 49d12eb73becef15d17f5a888c7a8240f6361e06 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:13:26 +0200 Subject: [PATCH 70/88] Update --- lib/src/configuration_values.dart | 5 ----- test/configuration_test.dart | 4 ---- 2 files changed, 9 deletions(-) diff --git a/lib/src/configuration_values.dart b/lib/src/configuration_values.dart index 68710c37..a0d552d1 100644 --- a/lib/src/configuration_values.dart +++ b/lib/src/configuration_values.dart @@ -165,15 +165,10 @@ class ConfigurationValues { if (envSentryCliCdnUrl?.isEmpty ?? false) { envSentryCliCdnUrl = null; } - String? envDartSymbolMapPath = environment['SENTRY_DART_SYMBOL_MAP_PATH']; - if (envDartSymbolMapPath?.isEmpty ?? false) { - envDartSymbolMapPath = null; - } return ConfigurationValues( release: envRelease, dist: envDist, sentryCliCdnUrl: envSentryCliCdnUrl, - dartSymbolMapPath: envDartSymbolMapPath, ); } diff --git a/test/configuration_test.dart b/test/configuration_test.dart index a1bb1398..9f2808b5 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -25,19 +25,16 @@ void main() { release: 'release-platformEnv-config', dist: 'dist-platformEnv-config', sentryCliCdnUrl: 'sentryCliCdnUrl-platformEnv-config', - dartSymbolMapPath: 'dart-symbol-map-platformEnv-config', ); final argsConfig = ConfigurationValues( release: 'release-args-config', dist: 'dist-args-config', sentryCliCdnUrl: 'sentryCliCdnUrl-args-config', - dartSymbolMapPath: 'dart-symbol-map-args-config', ); final fileConfig = ConfigurationValues( release: 'release-file-config', dist: 'dist-file-config', sentryCliCdnUrl: 'sentryCliCdnUrl-file-config', - dartSymbolMapPath: 'dart-symbol-map-file-config', ); final sut = fixture.getSut( @@ -49,7 +46,6 @@ void main() { expect(sut.release, 'release-platformEnv-config'); expect(sut.dist, 'dist-platformEnv-config'); expect(sut.sentryCliCdnUrl, 'sentryCliCdnUrl-platformEnv-config'); - expect(sut.dartSymbolMapPath, 'dart-symbol-map-platformEnv-config'); }); // env config From 1f31f279516290ffbf85d8246048adaf6a028085 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:14:37 +0200 Subject: [PATCH 71/88] Update --- lib/sentry_dart_plugin.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index fead503a..df4ba8c3 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -147,8 +147,8 @@ class SentryDartPlugin { return params; } - /// Guarded implementation for uploading Dart symbol map alongside each relevant debug file. - /// Currently a no-op until `_dartSymbolMapUploadEnabled` is flipped to true. + /// Upload Dart symbol map(s) if configured. + /// This is needed to symbolicate Flutter issue titles for obfuscated builds. Future _tryUploadDartSymbolMap() async { const taskName = 'uploading Dart symbol map(s)'; Log.startingTask(taskName); From a1eeebed65faead6b50323961a78499b5a0d015d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:15:26 +0200 Subject: [PATCH 72/88] Update --- lib/src/utils/path_utils.dart | 25 ------------------- test/dart_symbol_map_path_resolving_test.dart | 2 +- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 lib/src/utils/path_utils.dart diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart deleted file mode 100644 index fe9acf79..00000000 --- a/lib/src/utils/path_utils.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:file/file.dart'; - -import 'log.dart'; - -/// Resolves a config-provided file path to an absolute path if the file exists. -Future resolveFilePath({ - required FileSystem fs, - required String? rawPath, - required String missingPathWarning, - required String fileNotFoundWarning, -}) async { - final String? providedPath = rawPath?.trim(); - if (providedPath == null || providedPath.isEmpty) { - Log.warn(missingPathWarning); - return null; - } - - final File file = fs.file(providedPath); - if (!await file.exists()) { - Log.warn(fileNotFoundWarning); - return null; - } - - return file.absolute.path; -} diff --git a/test/dart_symbol_map_path_resolving_test.dart b/test/dart_symbol_map_path_resolving_test.dart index 2bca5b22..6ab932d4 100644 --- a/test/dart_symbol_map_path_resolving_test.dart +++ b/test/dart_symbol_map_path_resolving_test.dart @@ -2,7 +2,7 @@ import 'package:file/memory.dart'; import 'package:test/test.dart'; import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/utils/path_utils.dart'; +import 'package:sentry_dart_plugin/src/utils/path.dart'; void main() { group('resolveFilePath for dartSymbolMapPath', () { From 811a8d4a4a1fbabafc66b33b844b413b2bce4cf7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:16:30 +0200 Subject: [PATCH 73/88] Update --- lib/sentry_dart_plugin.dart | 2 +- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index df4ba8c3..3e378a24 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -155,7 +155,7 @@ class SentryDartPlugin { try { final fs = injector.get(); - await uploadDartSymbolMaps(fs: fs, config: _configuration); + await uploadDartSymbolMap(fs: fs, config: _configuration); } catch (e) { Log.error('Dart symbol map upload failed: $e'); } finally { diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index ba319afc..6d9ff317 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -14,7 +14,7 @@ import 'dart_symbol_map_uploader.dart'; /// - Resolves the Dart symbol map path from config /// - Collects relevant debug files /// - Uploads the map once per debug file via the CLI -Future uploadDartSymbolMaps({ +Future uploadDartSymbolMap({ required FileSystem fs, required Configuration config, }) async { From fba593249ce3ec087da2aa621d2a194fdeba71cd Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:17:59 +0200 Subject: [PATCH 74/88] Update docs --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 5e2ecf1c..4a275c6f 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -24,7 +24,7 @@ class DartSymbolMapUploader { /// will reject maps with equivalent content. We artificially add a marker /// to the map to avoid this. See [_prependDebugIdMarkerToMapFile] for more /// details. When this is improved in Sentry we can remove the marker and - /// just upload the map. + /// just upload the map without modifications. /// /// Throws [ExitError] on the first non-zero CLI exit code. static Future upload({ From 777e17d586d58a6647cf2ca5e1e2c1dab95c429b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:28:06 +0200 Subject: [PATCH 75/88] Fix tests --- test/configuration_values_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/configuration_values_test.dart b/test/configuration_values_test.dart index d5432b03..cbf801ae 100644 --- a/test/configuration_values_test.dart +++ b/test/configuration_values_test.dart @@ -281,14 +281,12 @@ void main() { 'SENTRY_RELEASE': 'fixture-release', 'SENTRY_DIST': 'fixture-dist', 'SENTRYCLI_CDNURL': 'fixture-sentry_cli_cdn_url', - 'SENTRY_DART_SYMBOL_MAP_PATH': 'fixture-env-dart-symbol-map.json', }; final sut = ConfigurationValues.fromPlatformEnvironment(arguments); expect(sut.release, 'fixture-release'); expect(sut.dist, 'fixture-dist'); expect(sut.sentryCliCdnUrl, 'fixture-sentry_cli_cdn_url'); - expect(sut.dartSymbolMapPath, 'fixture-env-dart-symbol-map.json'); }); }); } From 9db5261c061342b5e0dfb73ef2efb034f5e936a9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:35:08 +0200 Subject: [PATCH 76/88] Update CHANGELOG --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46674fe..c0d11b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ ### Features -### Features - - Upload Dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) - What it does: Enables symbolication of Flutter issue titles for obfuscated Android, iOS, and macOS builds. Linux and Windows are not supported yet. - Generate the mapping file: Add `--extra-gen-snapshot-options=--save-obfuscation-map=` when building. Example: `flutter build apk --obfuscate --split-debug-info=build/symbols --extra-gen-snapshot-options=--save-obfuscation-map=build/mapping.json` From 0f899d59e2077adcb5bfcfd85321e1860d558868 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:39:56 +0200 Subject: [PATCH 77/88] Update --- README.md | 2 +- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 2 +- test/dart_map_uploader_test.dart | 6 +++--- test/dart_symbol_map_uploader_test.dart | 6 +++--- test/integration_test.dart | 3 ++- test/plugin_test.dart | 2 ++ 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 80c7bf46..636ef5cc 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ ignore_missing=true | build_path | The build folder of debug files for upload | `build` (string) | no | - | | web_build_path | The web build folder of debug files for upload relative to build_path | `web` (string) | no | - | | symbols_path | The directory containing debug symbols (i.e. the `--split-debug-info=` parameter value you pass to `flutter build`) | `.` (string) | no | - | -| dart_symbol_map_path | Absolute or relative path to a Dart obfuscation map file to upload alongside relevant Flutter debug files. The map file is generated by using `--obfuscate` and `--extra-gen-snapshot-options=--save-obfuscation-map=` during Flutter build. | null (string) | no | SENTRY_DART_SYMBOL_MAP_PATH | +| dart_symbol_map_path | Absolute or relative path to a Dart obfuscation map file to upload alongside relevant Flutter debug files. The map file is generated by using `--obfuscate` and `--extra-gen-snapshot-options=--save-obfuscation-map=` during Flutter build. | null (string) | no | - | | commits | Release commits integration | auto (string) | no | - | | ignore_missing | Ignore missing commits previously used in the release | false (boolean) | no | - | | bin_dir | The folder where the plugin downloads the sentry-cli binary | .dart_tool/pub/bin/sentry_dart_plugin (string) | no | - | diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index 6d9ff317..c806911d 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -39,7 +39,7 @@ Future uploadDartSymbolMap({ return; } - await DartSymbolMapUploader.upload( + await DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: resolvedMapPath, debugFilePaths: debugFiles, diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index 4a275c6f..f5d3659a 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -27,7 +27,7 @@ class DartSymbolMapUploader { /// just upload the map without modifications. /// /// Throws [ExitError] on the first non-zero CLI exit code. - static Future upload({ + static Future addDebugIdMarkerAndUpload({ required Configuration config, required String symbolMapPath, required Iterable debugFilePaths, diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart index 50a92c14..75bb735f 100644 --- a/test/dart_map_uploader_test.dart +++ b/test/dart_map_uploader_test.dart @@ -33,7 +33,7 @@ void main() { '/b/App.framework.dSYM/Contents/Resources/DWARF/App', ]; - await DartSymbolMapUploader.upload( + await DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -82,7 +82,7 @@ void main() { final map = '/m/map.json'; final debugFiles = ['/d/file.symbols']; - await DartSymbolMapUploader.upload( + await DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -111,7 +111,7 @@ void main() { ..authToken = null ..logLevel = null; - final call = DartSymbolMapUploader.upload( + final call = DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: '/map.json', debugFilePaths: ['/debug.symbols', '/ignored.second'], diff --git a/test/dart_symbol_map_uploader_test.dart b/test/dart_symbol_map_uploader_test.dart index 50a92c14..75bb735f 100644 --- a/test/dart_symbol_map_uploader_test.dart +++ b/test/dart_symbol_map_uploader_test.dart @@ -33,7 +33,7 @@ void main() { '/b/App.framework.dSYM/Contents/Resources/DWARF/App', ]; - await DartSymbolMapUploader.upload( + await DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -82,7 +82,7 @@ void main() { final map = '/m/map.json'; final debugFiles = ['/d/file.symbols']; - await DartSymbolMapUploader.upload( + await DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: map, debugFilePaths: debugFiles, @@ -111,7 +111,7 @@ void main() { ..authToken = null ..logLevel = null; - final call = DartSymbolMapUploader.upload( + final call = DartSymbolMapUploader.addDebugIdMarkerAndUpload( config: config, symbolMapPath: '/map.json', debugFilePaths: ['/debug.symbols', '/ignored.second'], diff --git a/test/integration_test.dart b/test/integration_test.dart index c7ad8f8e..8b76f033 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -152,7 +152,8 @@ void main() async { platform == 'appbundle') { final hasSummary = pluginOutput.any( (e) => e.contains('Dart symbol map upload summary: attempted=')); - expect(hasSummary, isTrue); + expect(hasSummary, isTrue, + reason: 'Dart symbol map upload summary not found'); } }, timeout: Timeout(const Duration(minutes: 5))); } diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 29076361..8e7356d7 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -237,6 +237,8 @@ void main() { final commandLog = await runWith(version, config); const release = '$name@$version'; + print(commandLog); + final args = '$commonArgs --log-level debug'; expect( commandLog, From 1ee6f3d3858c3e96bc84ddf904df0398d35d73e5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:44:00 +0200 Subject: [PATCH 78/88] Update test --- test/integration_test.dart | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 8b76f033..8f360de6 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -19,15 +19,15 @@ final testPlatforms = Platform.environment.containsKey('TEST_PLATFORM') : [ 'apk', 'appbundle', - if (Platform.isMacOS) 'macos', - if (Platform.isMacOS) 'macos-framework', - if (Platform.isMacOS) 'ios', - if (Platform.isMacOS) 'ios-framework', - if (Platform.isMacOS) 'ipa', - if (Platform.isWindows) 'windows', - if (Platform.isLinux) 'linux', - 'web', - 'web-legacy' + // if (Platform.isMacOS) 'macos', + // if (Platform.isMacOS) 'macos-framework', + // if (Platform.isMacOS) 'ios', + // if (Platform.isMacOS) 'ios-framework', + // if (Platform.isMacOS) 'ipa', + // if (Platform.isWindows) 'windows', + // if (Platform.isLinux) 'linux', + // 'web', + // 'web-legacy' ]; // NOTE: Don't run/debug this main(), it likely won't work. @@ -225,7 +225,9 @@ Future _prepareTestApp(Directory tempDir, String platform) async { if (['ipa', 'ios'].contains(platform)) '--no-codesign', if (platform == 'web') '--source-maps', if (platform != 'web') '--split-debug-info=symbols', - if (platform != 'web') '--obfuscate' + if (platform != 'web') '--obfuscate', + if (platform != 'web') + '--extra-gen-snapshot-options=--save-obfuscation-map=obfuscation.map.json', ]; // In order to not run the build on every test execution, we store a hash. @@ -273,15 +275,6 @@ sentry: await hashFile.writeAsString(hash); } - // Ensure a Dart obfuscation map exists for non-web builds so the plugin can - // exercise the dart-symbol-map upload path during integration runs. - if (platform != 'web' && platform != 'web-legacy') { - final mapFile = File('${appDir.path}/obfuscation.map.json'); - if (!await mapFile.exists()) { - await mapFile.writeAsString('[]'); - } - } - if (isWebLegacy) { platform = 'web-legacy'; } From a79df66407b0ebf2465af11eb7a41f319aac59b6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:46:09 +0200 Subject: [PATCH 79/88] Remove duplicate --- test/dart_map_uploader_test.dart | 196 ------------------------------- 1 file changed, 196 deletions(-) delete mode 100644 test/dart_map_uploader_test.dart diff --git a/test/dart_map_uploader_test.dart b/test/dart_map_uploader_test.dart deleted file mode 100644 index 75bb735f..00000000 --- a/test/dart_map_uploader_test.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:process/process.dart'; -import 'package:test/test.dart'; - -import 'package:sentry_dart_plugin/src/configuration.dart'; -import 'package:sentry_dart_plugin/src/symbol_maps/dart_symbol_map_uploader.dart'; -import 'package:sentry_dart_plugin/src/utils/injector.dart'; -import 'package:sentry_dart_plugin/src/utils/log.dart'; - -void main() { - group('DartMapUploader.upload', () { - late MockProcessManager pm; - - setUp(() { - pm = MockProcessManager(); - injector.registerSingleton(() => pm, override: true); - }); - - test('emits one command per debug file with all flags', () async { - final config = Configuration() - ..cliPath = 'mock-cli' - ..url = 'https://example.invalid' - ..authToken = 'token' - ..logLevel = 'debug' - ..org = 'my-org' - ..project = 'my-proj'; - - final map = '/abs/path/obfuscation.map'; - final debugFiles = [ - '/a/app.android-arm.symbols', - '/b/App.framework.dSYM/Contents/Resources/DWARF/App', - ]; - - await DartSymbolMapUploader.addDebugIdMarkerAndUpload( - config: config, - symbolMapPath: map, - debugFilePaths: debugFiles, - ); - - expect(pm.commandLog.length, 4); - expect( - pm.commandLog[0], - equals( - 'mock-cli debug-files check --json ${debugFiles[0]}', - ), - ); - expect( - pm.commandLog[1], - equals( - 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj ' - '$map ${debugFiles[0]}', - ), - ); - expect( - pm.commandLog[2], - equals( - 'mock-cli debug-files check --json ${debugFiles[1]}', - ), - ); - expect( - pm.commandLog[3], - equals( - 'mock-cli --url https://example.invalid --auth-token token --log-level debug ' - 'dart-symbol-map upload --org my-org --project my-proj ' - '$map ${debugFiles[1]}', - ), - ); - }); - - test('omits optional flags when not configured', () async { - final config = Configuration() - ..cliPath = 'mock-cli' - ..url = null - ..authToken = null - ..logLevel = null - ..org = null - ..project = null; - - final map = '/m/map.json'; - final debugFiles = ['/d/file.symbols']; - - await DartSymbolMapUploader.addDebugIdMarkerAndUpload( - config: config, - symbolMapPath: map, - debugFilePaths: debugFiles, - ); - - expect(pm.commandLog.length, 2); - expect( - pm.commandLog[0], - equals('mock-cli debug-files check --json ${debugFiles.single}'), - ); - expect( - pm.commandLog[1], - equals('mock-cli dart-symbol-map upload $map ${debugFiles.single}'), - ); - }); - - test('propagates non-zero exit codes via ExitError', () async { - // First debug-id check succeeds, then the first upload fails. - pm.exitCodes = [0, 1]; - - final config = Configuration() - ..cliPath = 'mock-cli' - ..org = 'o' - ..project = 'p' - ..url = null - ..authToken = null - ..logLevel = null; - - final call = DartSymbolMapUploader.addDebugIdMarkerAndUpload( - config: config, - symbolMapPath: '/map.json', - debugFilePaths: ['/debug.symbols', '/ignored.second'], - ); - - await expectLater(call, throwsA(isA())); - // Only the first pair of commands (check + upload) should have been issued - // because the first upload fails and throws. - expect(pm.commandLog.length, 2); - }); - }); -} - -class MockProcessManager implements ProcessManager { - final List commandLog = []; - List exitCodes = []; // optional per-start exit codes - - @override - bool canRun(executable, {String? workingDirectory}) => true; - - @override - bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) => true; - - @override - Future run(List command, - {String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - covariant Encoding? stdoutEncoding = systemEncoding, - covariant Encoding? stderrEncoding = systemEncoding}) async { - return runSync(command); - } - - @override - ProcessResult runSync(List command, - {String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - covariant Encoding? stdoutEncoding = systemEncoding, - covariant Encoding? stderrEncoding = systemEncoding}) { - commandLog.add(command.join(' ')); - final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; - return ProcessResult(-1, code, null, null); - } - - @override - Future start(List command, - {String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - ProcessStartMode mode = ProcessStartMode.normal}) async { - commandLog.add(command.join(' ')); - final int code = exitCodes.isNotEmpty ? exitCodes.removeAt(0) : 0; - return MockProcess(code); - } -} - -class MockProcess implements Process { - final int _exitCode; - MockProcess(this._exitCode); - - @override - Future get exitCode => Future.value(_exitCode); - - @override - bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => false; - - @override - int get pid => -1; - - @override - Stream> get stderr => const Stream>.empty(); - - @override - IOSink get stdin => throw UnimplementedError(); - - @override - Stream> get stdout => const Stream>.empty(); -} From 3224d423fdcc3acab02190c0cf364d2fb4f5eeff Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:51:49 +0200 Subject: [PATCH 80/88] Cursor review --- test/integration_test.dart | 18 +++++++++--------- test/plugin_test.dart | 2 -- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 8f360de6..4020fc41 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -19,15 +19,15 @@ final testPlatforms = Platform.environment.containsKey('TEST_PLATFORM') : [ 'apk', 'appbundle', - // if (Platform.isMacOS) 'macos', - // if (Platform.isMacOS) 'macos-framework', - // if (Platform.isMacOS) 'ios', - // if (Platform.isMacOS) 'ios-framework', - // if (Platform.isMacOS) 'ipa', - // if (Platform.isWindows) 'windows', - // if (Platform.isLinux) 'linux', - // 'web', - // 'web-legacy' + if (Platform.isMacOS) 'macos', + if (Platform.isMacOS) 'macos-framework', + if (Platform.isMacOS) 'ios', + if (Platform.isMacOS) 'ios-framework', + if (Platform.isMacOS) 'ipa', + if (Platform.isWindows) 'windows', + if (Platform.isLinux) 'linux', + 'web', + 'web-legacy' ]; // NOTE: Don't run/debug this main(), it likely won't work. diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 8e7356d7..29076361 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -237,8 +237,6 @@ void main() { final commandLog = await runWith(version, config); const release = '$name@$version'; - print(commandLog); - final args = '$commonArgs --log-level debug'; expect( commandLog, From 0ef4ecfa85c135b6c945704ef0ff38a1ab34437e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:52:44 +0200 Subject: [PATCH 81/88] Update marker --- lib/src/symbol_maps/dart_symbol_map_uploader.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index f5d3659a..f72e18ad 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -179,6 +179,8 @@ class DartSymbolMapUploader { } } + static const _debugIdMarker = 'SENTRY_DEBUG_ID_MARKER'; + /// Reads the Dart symbol map at [mapPath] and ensures the array starts with /// ["SENTRY_DEBUG_ID_MARKER", debugId]. If a previous marker is present, it /// will be replaced. @@ -201,15 +203,23 @@ class DartSymbolMapUploader { } final List original = List.from(decoded); + + // If the file already has the same marker, do nothing to keep it untouched. + if (original.length >= 2 && + original[0] == _debugIdMarker && + original[1] == debugId) { + return; + } + List tail; - if (original.isNotEmpty && original.first == 'SENTRY_DEBUG_ID_MARKER') { + if (original.isNotEmpty && original.first == _debugIdMarker) { tail = original.length > 2 ? original.sublist(2) : []; } else { tail = original; } final List updated = [ - 'SENTRY_DEBUG_ID_MARKER', + _debugIdMarker, debugId, ...tail, ]; From 3b3af39ed0a291a97864bbd60365d0cccea39d1e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 15:54:37 +0200 Subject: [PATCH 82/88] Update comment --- lib/src/symbol_maps/dart_symbol_map.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index c806911d..ffc6ca1d 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -8,7 +8,9 @@ import 'dart_symbol_map_uploader.dart'; /// Single entrypoint to upload Dart obfuscation map(s) paired with /// Flutter-relevant native debug files. This obfuscation map is used to -/// symbolicate Flutter issue titles for non-web platforms. +/// symbolicate Flutter issue titles. +/// +/// Currently only supported for Android, iOS and macOS. /// /// Steps: /// - Resolves the Dart symbol map path from config From 4a6127444631ccf482a6c476e037ddd2e02a1518 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 16:34:54 +0200 Subject: [PATCH 83/88] Review --- ...dart_symbol_map_debug_files_collector.dart | 31 ++----------------- .../symbol_maps/dart_symbol_map_uploader.dart | 11 +++++-- lib/src/utils/sentry_cli_args.dart | 1 - 3 files changed, 11 insertions(+), 32 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart index f8d314dd..60c57f49 100644 --- a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart @@ -41,37 +41,12 @@ Future> collectDebugFilesForDartMap({ } } - Future containsAndroidSymbols(String rootPath) async { - if (rootPath.isEmpty) return false; - final Directory directory = fs.directory(rootPath); - if (!await directory.exists()) return false; - - await for (final FileSystemEntity entity - in directory.list(recursive: true, followLinks: false)) { - if (entity is! File) continue; - final String basename = fs.path.basename(entity.path); - if (basename.startsWith('app') && - basename.endsWith('.symbols') && - !basename.contains('darwin') && - !basename.contains('ios')) { - return true; - } - } - return false; - } - - // No recursive Apple scan needed. We only care about the Mach-O at - // /App.framework.dSYM/Contents/Resources/DWARF/App. For roots that - // end with 'Runner.app', the dSYM lives next to it, so we probe the parent - // directory as the base. - - // Prefer scanning Android symbols only under the configured symbols folder. + // Prefer scanning Android symbols under the configured symbols folder; if not + // set, fall back to the build folder. final List androidRoots = []; - if (config.symbolsFolder.isNotEmpty && - await containsAndroidSymbols(config.symbolsFolder)) { + if (config.symbolsFolder.isNotEmpty) { androidRoots.add(path.normalize(config.symbolsFolder)); } else if (config.buildFilesFolder.isNotEmpty) { - // Fallback if symbolsFolder is not provided or does not contain any symbols. androidRoots.add(path.normalize(config.buildFilesFolder)); } diff --git a/lib/src/symbol_maps/dart_symbol_map_uploader.dart b/lib/src/symbol_maps/dart_symbol_map_uploader.dart index f72e18ad..d8f99234 100644 --- a/lib/src/symbol_maps/dart_symbol_map_uploader.dart +++ b/lib/src/symbol_maps/dart_symbol_map_uploader.dart @@ -32,6 +32,12 @@ class DartSymbolMapUploader { required String symbolMapPath, required Iterable debugFilePaths, }) async { + final cliPath = config.cliPath; + if (cliPath == null) { + Log.warn('Skipping Dart symbol map uploads: no CLI path provided.'); + return; + } + final ProcessManager processManager = injector.get(); int attempted = 0; @@ -44,7 +50,7 @@ class DartSymbolMapUploader { final String? debugId = await _fetchDebugId( processManager: processManager, - cliPath: config.cliPath!, + cliPath: cliPath, debugFilePath: debugFilePath, ); if (debugId != null && debugId.isNotEmpty) { @@ -68,7 +74,7 @@ class DartSymbolMapUploader { final int exitCode = await _startAndForward( processManager: processManager, - cliPath: config.cliPath!, + cliPath: cliPath, args: args, errorContext: 'Failed to upload Dart symbol map for $debugFilePath', ); @@ -89,7 +95,6 @@ class DartSymbolMapUploader { } /// Starts the process and forwards stdout/stderr to [Log]. Returns exit code. - /// TODO(buenaflor): write a utility function for reuse in other parts of the code. static Future _startAndForward({ required ProcessManager processManager, required String cliPath, diff --git a/lib/src/utils/sentry_cli_args.dart b/lib/src/utils/sentry_cli_args.dart index e83272dd..ced61908 100644 --- a/lib/src/utils/sentry_cli_args.dart +++ b/lib/src/utils/sentry_cli_args.dart @@ -1,6 +1,5 @@ import '../configuration.dart'; -// TODO(buenaflor): in a future PR this should be reused in other parts of the code extension SentryCliArgs on Configuration { List baseArgs() => [ if (url != null) ...['--url', url!], From 7919595d7c815f80143cf920ac4c86f4480b5374 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 16:41:03 +0200 Subject: [PATCH 84/88] Update CI --- .github/workflows/integration-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 9a96a28c..99cab6dd 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -58,6 +58,9 @@ jobs: path: temp/testapp-${{ matrix.target }} key: integration-test-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('flutter.version') }} + - run: sudo xcode-select --switch /Applications/Xcode_16.4.app + if: ${{ matrix.target == 'ios' || matrix.target == 'ios-framework' || matrix.target == 'ipa' }} + - run: dart test --tags integration - uses: actions/upload-artifact@v4 From 7c56763579db01d72df5aeef599248030ae7690c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 16:49:23 +0200 Subject: [PATCH 85/88] Update --- test/integration_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 4020fc41..15809f06 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -145,9 +145,8 @@ void main() async { // Ensure that when a map is present we exercise the dart-symbol-map path for supported platforms. if (platform == 'ios' || - platform == 'ios-framework' || platform == 'macos' || - platform == 'macos-framework' || + platform == 'ipa' || platform == 'apk' || platform == 'appbundle') { final hasSummary = pluginOutput.any( From f81d281bc60683f58b0c19ba2a322e7edb9f26b2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 19:51:06 +0200 Subject: [PATCH 86/88] Update --- lib/src/symbol_maps/dart_symbol_map.dart | 2 +- ...dart_symbol_map_debug_files_collector.dart | 71 +++++++++++-------- ...symbol_map_debug_files_collector_test.dart | 34 ++++----- 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/lib/src/symbol_maps/dart_symbol_map.dart b/lib/src/symbol_maps/dart_symbol_map.dart index ffc6ca1d..f3caa3eb 100644 --- a/lib/src/symbol_maps/dart_symbol_map.dart +++ b/lib/src/symbol_maps/dart_symbol_map.dart @@ -10,7 +10,7 @@ import 'dart_symbol_map_uploader.dart'; /// Flutter-relevant native debug files. This obfuscation map is used to /// symbolicate Flutter issue titles. /// -/// Currently only supported for Android, iOS and macOS. +/// Currently only supported for Android (apk/appbundle) and iOS (ios/ipa). /// /// Steps: /// - Resolves the Dart symbol map path from config diff --git a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart index 60c57f49..7ec8ffda 100644 --- a/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart +++ b/lib/src/symbol_maps/dart_symbol_map_debug_files_collector.dart @@ -1,5 +1,6 @@ import 'package:file/file.dart'; -import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; +import 'package:sentry_dart_plugin/src/utils/log.dart'; +// import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; import '../configuration.dart'; @@ -19,7 +20,8 @@ Future> collectDebugFilesForDartMap({ required FileSystem fs, required Configuration config, }) async { - final Set foundPaths = {}; + final Set foundAndroidPaths = {}; + final Set foundIosPaths = {}; final path = fs.path; Future collectAndroidSymbolsUnder(String rootPath) async { @@ -36,7 +38,7 @@ Future> collectDebugFilesForDartMap({ basename.endsWith('.symbols') && !basename.contains('darwin') && !basename.contains('ios')) { - foundPaths.add(fs.file(entity.path).absolute.path); + foundAndroidPaths.add(fs.file(entity.path).absolute.path); } } } @@ -54,34 +56,47 @@ Future> collectDebugFilesForDartMap({ await collectAndroidSymbolsUnder(root); } - // Enumerate Flutter-related roots and only consider those that contain - // 'ios' or 'macos'. Compute the expected Mach-O path directly for each. - await for (final String root - in enumerateDebugSearchRoots(fs: fs, config: config)) { - final String normalized = path.normalize(root); - final String lower = normalized.toLowerCase(); - if (!(lower.contains('ios') || lower.contains('macos'))) { - continue; - } + if (foundAndroidPaths.isEmpty) { + Log.warn( + 'No Android symbols found in the configured symbols folder or build folder.'); + } + + // iOS: only search under two roots to simplify discovery: + // - build/ios + // - /ios/build (Fastlane) + final String buildDir = config.buildFilesFolder; + final String projectRoot = fs.currentDirectory.path; + + Future collectIosAppDsymsUnderRoot(String rootPath) async { + if (rootPath.isEmpty) return; + final Directory directory = fs.directory(rootPath); + if (!await directory.exists()) return; - for (final String candidateBase in { - normalized, - path.dirname(normalized), - }) { - final String machOPath = path.join( - candidateBase, - 'App.framework.dSYM', - 'Contents', - 'Resources', - 'DWARF', - 'App', - ); - final File machOFile = fs.file(machOPath); - if (await machOFile.exists()) { - foundPaths.add(machOFile.absolute.path); + final String dsymSuffix = path.join( + 'App.framework.dSYM', + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + + await for (final FileSystemEntity entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String normalized = path.normalize(entity.path); + if (normalized.endsWith(dsymSuffix)) { + foundIosPaths.add(fs.file(normalized).absolute.path); } } } - return foundPaths; + await collectIosAppDsymsUnderRoot(path.join(path.normalize(buildDir), 'ios')); + await collectIosAppDsymsUnderRoot(path.join(projectRoot, 'ios', 'build')); + + if (foundIosPaths.isEmpty) { + Log.warn( + 'No iOS symbols found in the configured build folder or project root.'); + } + + return foundAndroidPaths.union(foundIosPaths).toSet(); } diff --git a/test/dart_symbol_map_debug_files_collector_test.dart b/test/dart_symbol_map_debug_files_collector_test.dart index 16a60294..1273486c 100644 --- a/test/dart_symbol_map_debug_files_collector_test.dart +++ b/test/dart_symbol_map_debug_files_collector_test.dart @@ -97,29 +97,21 @@ void main() { expect(result, contains(fs.path.normalize(machO))); }); - test('finds App.framework.dSYM in macOS build products', () async { + // macOS is not supported for Dart symbol map pairing. + + test('finds App.framework.dSYM inside iOS Xcode archive dSYMs', () async { final fs = MemoryFileSystem(style: FileSystemStyle.posix); - final projectRootDir = fs.directory('/macosproj') + final projectRootDir = fs.directory('/iosproj') ..createSync(recursive: true); fs.currentDirectory = projectRootDir; - final buildDir = '/macosproj/build'; - final symbolsDir = '/macosproj/symbols'; + final buildDir = '/iosproj/build'; + final symbolsDir = '/iosproj/symbols'; - // macOS Products Release path - final macMachO = - '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; - fs.file(macMachO).createSync(recursive: true); - - // Noise: other dSYMs should be ignored - fs - .file( - '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') - .createSync(recursive: true); - fs - .file( - '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') - .createSync(recursive: true); + // iOS archive path + final iosArchiveMachO = + '$buildDir/ios/archive/Runner.xcarchive/dSYMs/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(iosArchiveMachO).createSync(recursive: true); final config = Configuration() ..buildFilesFolder = buildDir @@ -130,11 +122,11 @@ void main() { config: config, ); - expect(result, contains(fs.path.normalize(macMachO))); - expect(result.any((p) => p.endsWith('/Runner')), isFalse); - expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + expect(result, contains(fs.path.normalize(iosArchiveMachO))); }); + // macOS archive is not supported for Dart symbol map pairing. + test('returns empty set when no roots or symbols exist', () async { final fs = MemoryFileSystem(style: FileSystemStyle.posix); final projectRootDir = fs.directory('/empty') From e67917a9c433b4ea11a154023b13a669f79b3796 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 19:51:29 +0200 Subject: [PATCH 87/88] Update --- test/integration_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration_test.dart b/test/integration_test.dart index 15809f06..5a261027 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -145,7 +145,6 @@ void main() async { // Ensure that when a map is present we exercise the dart-symbol-map path for supported platforms. if (platform == 'ios' || - platform == 'macos' || platform == 'ipa' || platform == 'apk' || platform == 'appbundle') { From 08b96901a3fe4e6fb7e562ec5080cb79400d1c9e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 14 Aug 2025 20:23:33 +0200 Subject: [PATCH 88/88] Update --- CHANGELOG.md | 4 +++- README.md | 50 +++++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d11b45..7c6a3ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ### Features - Upload Dart symbol mapping file ([#347](https://github.com/getsentry/sentry-dart-plugin/pull/347)) - - What it does: Enables symbolication of Flutter issue titles for obfuscated Android, iOS, and macOS builds. Linux and Windows are not supported yet. + - Enables symbolication of Flutter issue titles for obfuscated builds. + - Supported: Android and iOS + - Not supported (yet): macOS, Linux and Windows. - Generate the mapping file: Add `--extra-gen-snapshot-options=--save-obfuscation-map=` when building. Example: `flutter build apk --obfuscate --split-debug-info=build/symbols --extra-gen-snapshot-options=--save-obfuscation-map=build/mapping.json` - Configure the plugin: Set `dart_symbol_map_path: build/mapping.json` - Important: `dart_symbol_map_path` must point directly to the mapping file (absolute or relative path), not a directory. diff --git a/README.md b/README.md index 636ef5cc..9581278a 100644 --- a/README.md +++ b/README.md @@ -101,31 +101,31 @@ ignore_missing=true ### Available Configuration Fields -| Configuration Name | Description | Default Value And Type | Required | Alternative Environment variable | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------- | -------------------------------- | -| upload_debug_symbols | Enables or disables the automatic upload of debug symbols | true (boolean) | no | - | -| upload_source_maps | Enables or disables the automatic upload of source maps | false (boolean) | no | - | -| upload_sources | Does or doesn't include the source code of native code | false (boolean) | no | - | -| legacy_web_symbolication | Uses legacy symbolication method for Flutter Web instead of Debug IDs | false (boolean) | no | - | -| project | Project's name | e.g. sentry-flutter (string) | yes | SENTRY_PROJECT | -| org | Organization's slug | e.g. sentry-sdks (string) | yes | SENTRY_ORG | -| auth_token | Auth Token | e.g. 64 random characteres (string) | yes | SENTRY_AUTH_TOKEN | -| url | URL | e.g. https://mysentry.invalid/ (string) | no | SENTRY_URL | -| url_prefix | URL prefix for JS source maps | e.g. ~/app/ (string) | no | - | -| wait_for_processing | Wait for server-side processing of uploaded files | false (boolean) | no | - | -| log_level | Configures the log level for sentry-cli | warn (string) | no | SENTRY_LOG_LEVEL | -| release | The release version for source maps, it should match the release set by the SDK | name@version from pubspec (string) | no | SENTRY_RELEASE | -| dist | The dist/build number for source maps, it should match the dist set by the SDK | the number after the '+' char from 'version' pubspec (string) | no | SENTRY_DIST | -| build_path | The build folder of debug files for upload | `build` (string) | no | - | -| web_build_path | The web build folder of debug files for upload relative to build_path | `web` (string) | no | - | -| symbols_path | The directory containing debug symbols (i.e. the `--split-debug-info=` parameter value you pass to `flutter build`) | `.` (string) | no | - | -| dart_symbol_map_path | Absolute or relative path to a Dart obfuscation map file to upload alongside relevant Flutter debug files. The map file is generated by using `--obfuscate` and `--extra-gen-snapshot-options=--save-obfuscation-map=` during Flutter build. | null (string) | no | - | -| commits | Release commits integration | auto (string) | no | - | -| ignore_missing | Ignore missing commits previously used in the release | false (boolean) | no | - | -| bin_dir | The folder where the plugin downloads the sentry-cli binary | .dart_tool/pub/bin/sentry_dart_plugin (string) | no | - | -| bin_path | Path to the sentry-cli binary to use instead of downloading. Make sure to use the correct version. | null (string) | no | - | -| sentry_cli_cdn_url | Alternative place to download sentry-cli | https://downloads.sentry-cdn.com/sentry-cli (string) | no | SENTRYCLI_CDNURL | -| sentry_cli_version | Override the sentry-cli version that should be downloaded. | (string) | no | - | +| Configuration Name | Description | Default Value And Type | Required | Alternative Environment variable | +| ------------------------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------- | -------- | -------------------------------- | +| upload_debug_symbols | Enables or disables the automatic upload of debug symbols | true (boolean) | no | - | +| upload_source_maps | Enables or disables the automatic upload of source maps | false (boolean) | no | - | +| upload_sources | Does or doesn't include the source code of native code | false (boolean) | no | - | +| legacy_web_symbolication | Uses legacy symbolication method for Flutter Web instead of Debug IDs | false (boolean) | no | - | +| project | Project's name | e.g. sentry-flutter (string) | yes | SENTRY_PROJECT | +| org | Organization's slug | e.g. sentry-sdks (string) | yes | SENTRY_ORG | +| auth_token | Auth Token | e.g. 64 random characteres (string) | yes | SENTRY_AUTH_TOKEN | +| url | URL | e.g. https://mysentry.invalid/ (string) | no | SENTRY_URL | +| url_prefix | URL prefix for JS source maps | e.g. ~/app/ (string) | no | - | +| wait_for_processing | Wait for server-side processing of uploaded files | false (boolean) | no | - | +| log_level | Configures the log level for sentry-cli | warn (string) | no | SENTRY_LOG_LEVEL | +| release | The release version for source maps, it should match the release set by the SDK | name@version from pubspec (string) | no | SENTRY_RELEASE | +| dist | The dist/build number for source maps, it should match the dist set by the SDK | the number after the '+' char from 'version' pubspec (string) | no | SENTRY_DIST | +| build_path | The build folder of debug files for upload | `build` (string) | no | - | +| web_build_path | The web build folder of debug files for upload relative to build_path | `web` (string) | no | - | +| symbols_path | The directory containing debug symbols (i.e. the `--split-debug-info=` parameter value you pass to `flutter build`) | `.` (string) | no | - | +| dart_symbol_map_path | Absolute or relative path to a Dart obfuscation map file to upload. This allows symbolication of Flutter issue titles for Android and iOS. The map file is generated by adding the following arguments to your Flutter build command: `--extra-gen-snapshot-options=--save-obfuscation-map=`. | null (string) | no | - | +| commits | Release commits integration | auto (string) | no | - | +| ignore_missing | Ignore missing commits previously used in the release | false (boolean) | no | - | +| bin_dir | The folder where the plugin downloads the sentry-cli binary | .dart_tool/pub/bin/sentry_dart_plugin (string) | no | - | +| bin_path | Path to the sentry-cli binary to use instead of downloading. Make sure to use the correct version. | null (string) | no | - | +| sentry_cli_cdn_url | Alternative place to download sentry-cli | https://downloads.sentry-cdn.com/sentry-cli (string) | no | SENTRYCLI_CDNURL | +| sentry_cli_version | Override the sentry-cli version that should be downloaded. | (string) | no | - | ## Breaking Changes in v3.0.0