diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf89865..bc122a8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Features + +- Ignore path list for web ([#340](https://github.com/getsentry/sentry-dart-plugin/pull/340)) + - You can use the `ignore_web_source_paths` field: e.g `ignore_web_source_paths: [test/**/*.js]` + - This will ignore all specified files and directories from being uploaded + ## 3.1.1 ### Fixes diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index f8c0ff5a..61659394 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:file/file.dart'; +import 'package:glob/glob.dart'; import 'package:process/process.dart'; import 'package:sentry_dart_plugin/src/utils/extensions.dart'; @@ -225,46 +226,64 @@ class SentryDartPlugin { await _executeAndLog('Failed to set commits', params); } - Future> _findAllJsFilePaths() async { - final List jsFiles = []; + /// Returns every file inside [_configuration.webBuildFilesFolder] whose + /// path ends with [extension] **and** is *not* matched by an ignore glob. + /// + /// The generic type `T` lets the caller decide what they want back + /// (e.g. `String` path vs. `File` object). + Future> _collectWebFiles({ + required String extension, + required T Function(File) builder, + }) async { final fs = injector.get(); final webDir = fs.directory(_configuration.webBuildFilesFolder); - if (await webDir.exists()) { - await for (final entity - in webDir.list(recursive: true, followLinks: false)) { - if (entity is File && entity.path.toLowerCase().endsWith('.js')) { - jsFiles.add(entity.path); - } - } - } else { + // Fast-fail if the directory doesn’t exist. + if (!await webDir.exists()) { Log.warn( - 'Web build directory "${_configuration.webBuildFilesFolder}" does not exist, skipping JS file enumeration.', + 'Web build directory "${_configuration.webBuildFilesFolder}" does not exist, ' + 'skipping $extension enumeration.', ); + return []; } - return jsFiles; - } - Future> _findAllSourceMapFiles() async { - final List sourceMapFiles = []; - final fs = injector.get(); - final webDir = fs.directory(_configuration.webBuildFilesFolder); + // Compile ignore globs once instead of on every iteration. + final ignoreGlobs = + _configuration.ignoreWebSourcePaths.map((p) => Glob(p)).toList(); - if (await webDir.exists()) { - await for (final entity - in webDir.list(recursive: true, followLinks: false)) { - if (entity is File && entity.path.toLowerCase().endsWith('.js.map')) { - sourceMapFiles.add(entity.absolute); - } + bool shouldIgnore(String relative) => + ignoreGlobs.any((g) => g.matches(relative)); + + final results = []; + + await for (final entity + in webDir.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + + final path = entity.path; + if (!path.toLowerCase().endsWith(extension)) continue; + + final relative = + fs.path.relative(path, from: _configuration.webBuildFilesFolder); + + if (!shouldIgnore(relative)) { + results.add(builder(entity)); } - } else { - Log.warn( - 'Web build directory "${_configuration.webBuildFilesFolder}" does not exist, skipping source map file enumeration.', - ); } - return sourceMapFiles; + + return results; } + Future> _findAllJsFilePaths() => _collectWebFiles( + extension: '.js', + builder: (file) => file.path, + ); + + Future> _findAllSourceMapFiles() => _collectWebFiles( + extension: '.js.map', + builder: (file) => file.absolute, + ); + Future _injectDebugIds() async { List params = []; params.add('sourcemaps'); @@ -331,6 +350,11 @@ class SentryDartPlugin { params.add('dart'); } + for (final ignorePattern in _configuration.ignoreWebSourcePaths) { + params.add('--ignore'); + params.add(ignorePattern); + } + params.addAll(_baseCliParams()); await _executeAndLog('Failed to sources files', params); diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index e0723502..1b23d4bb 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -106,6 +106,8 @@ class Configuration { /// Whether to use legacy web symbolication. Defaults to `false`. late bool legacyWebSymbolication; + late List ignoreWebSourcePaths; + /// Loads the configuration values Future getConfigValues(List cliArguments) async { const taskName = 'reading config values'; @@ -168,6 +170,7 @@ class Configuration { 'https://downloads.sentry-cdn.com/sentry-cli'; sentryCliVersion = configValues.sentryCliVersion; legacyWebSymbolication = configValues.legacyWebSymbolication ?? false; + ignoreWebSourcePaths = configValues.ignoreWebSourcePaths ?? []; } /// Validates the configuration values and log an error if required fields diff --git a/lib/src/configuration_values.dart b/lib/src/configuration_values.dart index 083a4aff..c8e5d691 100644 --- a/lib/src/configuration_values.dart +++ b/lib/src/configuration_values.dart @@ -27,6 +27,7 @@ class ConfigurationValues { final String? sentryCliCdnUrl; final String? sentryCliVersion; final bool? legacyWebSymbolication; + final List? ignoreWebSourcePaths; ConfigurationValues({ this.version, @@ -53,6 +54,7 @@ class ConfigurationValues { this.sentryCliCdnUrl, this.sentryCliVersion, this.legacyWebSymbolication, + this.ignoreWebSourcePaths, }); factory ConfigurationValues.fromArguments(List arguments) { @@ -108,6 +110,7 @@ class ConfigurationValues { legacyWebSymbolication: boolFromString( sentryArguments['legacy_web_symbolication'], ), + ignoreWebSourcePaths: sentryArguments['ignore_web_source_paths']?.split(',').map((e) => e.trim()).toList(), ); } @@ -143,6 +146,7 @@ class ConfigurationValues { sentryCliCdnUrl: configReader.getString('sentry_cli_cdn_url'), sentryCliVersion: configReader.getString('sentry_cli_version'), legacyWebSymbolication: configReader.getBool('legacy_web_symbolication'), + ignoreWebSourcePaths: configReader.getList('ignore_web_source_paths'), ); } @@ -201,6 +205,7 @@ class ConfigurationValues { sentryCliVersion: args.sentryCliVersion ?? file.sentryCliVersion, legacyWebSymbolication: args.legacyWebSymbolication ?? file.legacyWebSymbolication, + ignoreWebSourcePaths: args.ignoreWebSourcePaths ?? file.ignoreWebSourcePaths, ); } } diff --git a/lib/src/utils/config-reader/config_reader.dart b/lib/src/utils/config-reader/config_reader.dart index adbca101..192ed77c 100644 --- a/lib/src/utils/config-reader/config_reader.dart +++ b/lib/src/utils/config-reader/config_reader.dart @@ -12,6 +12,7 @@ import 'yaml_config_reader.dart'; abstract class ConfigReader { String? getString(String key, {String? deprecatedKey}); bool? getBool(String key, {String? deprecatedKey}); + List? getList(String key, {String? deprecatedKey}); bool contains(String key); /// This factory will try to load both pubspec.yaml and sentry.properties. diff --git a/lib/src/utils/config-reader/fallback_config_reader.dart b/lib/src/utils/config-reader/fallback_config_reader.dart index 7795f51e..015b2946 100644 --- a/lib/src/utils/config-reader/fallback_config_reader.dart +++ b/lib/src/utils/config-reader/fallback_config_reader.dart @@ -18,6 +18,12 @@ class FallbackConfigReader implements ConfigReader { _fallbackConfigReader?.getString(key, deprecatedKey: deprecatedKey); } + @override + List? getList(String key, {String? deprecatedKey}) { + return _configReader?.getList(key, deprecatedKey: deprecatedKey) ?? + _fallbackConfigReader?.getList(key, deprecatedKey: deprecatedKey); + } + @override bool contains(String key) { return _configReader?.contains(key) ?? diff --git a/lib/src/utils/config-reader/no_op_config_reader.dart b/lib/src/utils/config-reader/no_op_config_reader.dart index e792e28e..c5540a88 100644 --- a/lib/src/utils/config-reader/no_op_config_reader.dart +++ b/lib/src/utils/config-reader/no_op_config_reader.dart @@ -13,6 +13,11 @@ class NoOpConfigReader implements ConfigReader { return null; } + @override + List? getList(String key, {String? deprecatedKey}) { + return null; + } + @override bool contains(String key) { return false; diff --git a/lib/src/utils/config-reader/properties_config_reader.dart b/lib/src/utils/config-reader/properties_config_reader.dart index 22d25b10..05732975 100644 --- a/lib/src/utils/config-reader/properties_config_reader.dart +++ b/lib/src/utils/config-reader/properties_config_reader.dart @@ -17,6 +17,17 @@ class PropertiesConfigReader implements ConfigReader { return get(key, deprecatedKey, (key) => _properties.get((key))); } + @override + List? getList(String key, {String? deprecatedKey}) { + return get(key, deprecatedKey, (key) { + final value = _properties.get(key); + if (value != null && value.isNotEmpty) { + return value.split(',').map((e) => e.trim()).toList(); + } + return null; + }); + } + @override bool contains(String key) { return _properties.contains(key); diff --git a/lib/src/utils/config-reader/yaml_config_reader.dart b/lib/src/utils/config-reader/yaml_config_reader.dart index 5ae24313..b4aebc92 100644 --- a/lib/src/utils/config-reader/yaml_config_reader.dart +++ b/lib/src/utils/config-reader/yaml_config_reader.dart @@ -17,6 +17,19 @@ class YamlConfigReader implements ConfigReader { return get(key, deprecatedKey, (key) => (_yamlMap?[key]).toString()); } + @override + List? getList(String key, {String? deprecatedKey}) { + return get(key, deprecatedKey, (key) { + final value = _yamlMap?[key]; + if (value is YamlList) { + return value.map((e) => e.toString()).toList(); + } else if (value is List) { + return value.map((e) => e.toString()).toList(); + } + return null; + }); + } + @override bool contains(String key) { return _yamlMap?.containsKey(key) ?? false; diff --git a/pubspec.yaml b/pubspec.yaml index 452585ae..a8e1e314 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: convert: ^3.0.2 process: '>=4.2.4 <6.0.0' properties: ^2.1.0 + glob: ^2.1.3 dev_dependencies: lints: '>=3.0.0' diff --git a/test/configuration_test.dart b/test/configuration_test.dart index 10fe2df0..fde1f023 100644 --- a/test/configuration_test.dart +++ b/test/configuration_test.dart @@ -77,6 +77,7 @@ void main() { sentryCliCdnUrl: 'sentryCliCdnUrl-args-config', sentryCliVersion: '1.0.0-args-config', legacyWebSymbolication: true, + ignoreWebSourcePaths: ['some/', '**/*.js'], ); final fileConfig = ConfigurationValues( version: 'version-file-config', @@ -103,6 +104,7 @@ void main() { sentryCliCdnUrl: 'sentryCliCdnUrl-file-config', sentryCliVersion: '1.0.0-file-config', legacyWebSymbolication: false, + ignoreWebSourcePaths: ['some-other/'], ); final sut = fixture.getSut( @@ -141,6 +143,7 @@ void main() { expect(sut.sentryCliCdnUrl, 'sentryCliCdnUrl-args-config'); expect(sut.sentryCliVersion, '1.0.0-args-config'); expect(sut.legacyWebSymbolication, isTrue); + expect(sut.ignoreWebSourcePaths, ['some/', '**/*.js']); }); test("takes values from file config", () { @@ -171,6 +174,7 @@ void main() { sentryCliCdnUrl: 'sentryCliCdnUrl-file-config', sentryCliVersion: '1.0.0-file-config', legacyWebSymbolication: true, + ignoreWebSourcePaths: ['some/', '**/*.js'], ); final sut = fixture.getSut( @@ -208,6 +212,7 @@ void main() { expect(sut.sentryCliCdnUrl, 'sentryCliCdnUrl-file-config'); expect(sut.sentryCliVersion, '1.0.0-file-config'); expect(sut.legacyWebSymbolication, isTrue); + expect(sut.ignoreWebSourcePaths, ['some/', '**/*.js']); }); test("falls back to default values", () { @@ -240,6 +245,7 @@ void main() { 'https://downloads.sentry-cdn.com/sentry-cli', ); expect(sut.legacyWebSymbolication, isFalse); + expect(sut.ignoreWebSourcePaths, []); }); }); } diff --git a/test/configuration_values_test.dart b/test/configuration_values_test.dart index 59725141..c813a370 100644 --- a/test/configuration_values_test.dart +++ b/test/configuration_values_test.dart @@ -35,7 +35,8 @@ void main() { "--sentry-define=bin_dir=fixture-bin_dir", "--sentry-define=sentry_cli_cdn_url=fixture-sentry_cli_cdn_url", "--sentry-define=sentry_cli_version=1.0.0", - "--sentry-define=legacy_web_symbolication=true" + "--sentry-define=legacy_web_symbolication=true", + "--sentry-define=ignore_web_source_paths=some/, **/*.js" ]; final sut = ConfigurationValues.fromArguments(arguments); expect(sut.name, 'fixture-sentry-name'); @@ -60,6 +61,7 @@ void main() { expect(sut.sentryCliCdnUrl, 'fixture-sentry_cli_cdn_url'); expect(sut.sentryCliVersion, '1.0.0'); expect(sut.legacyWebSymbolication, isTrue); + expect(sut.ignoreWebSourcePaths, ['some/', '**/*.js']); }); test("fromArguments supports deprecated fields", () { @@ -103,6 +105,9 @@ void main() { sentry_cli_cdn_url: fixture-sentry_cli_cdn_url sentry_cli_version: 1.0.0 legacy_web_symbolication: true + ignore_web_source_paths: + - some/ + - '**/*.js' '''; FileSystem fs = MemoryFileSystem.test(); @@ -145,6 +150,7 @@ void main() { expect(sut.binDir, 'fixture-bin_dir'); expect(sut.sentryCliCdnUrl, 'fixture-sentry_cli_cdn_url'); expect(sut.legacyWebSymbolication, isTrue); + expect(sut.ignoreWebSourcePaths, ['some/', '**/*.js']); }); test('from config reader as properties', () { @@ -167,6 +173,7 @@ void main() { bin_dir=fixture-bin_dir sentry_cli_cdn_url=fixture-sentry_cli_cdn_url sentry_cli_version=1.0.0 + ignore_web_source_paths=[some/, **/*.js] '''; FileSystem fs = MemoryFileSystem.test(); @@ -209,6 +216,7 @@ void main() { expect(sut.binDir, 'fixture-bin_dir'); expect(sut.sentryCliCdnUrl, 'fixture-sentry_cli_cdn_url'); expect(sut.sentryCliVersion, '1.0.0'); + expect(sut.ignoreWebSourcePaths, ['some/', '**/*.js']); }); test('from config reader pubspec & properties', () { diff --git a/test/plugin_test.dart b/test/plugin_test.dart index 3dc8a715..072d10bf 100644 --- a/test/plugin_test.dart +++ b/test/plugin_test.dart @@ -85,7 +85,6 @@ void main() { setUp(() { createJsFilesForTesting(); }); - test('works with all configuration files', () async { const version = '1.0.0'; final config = ''' @@ -94,6 +93,7 @@ void main() { upload_source_maps: true log_level: debug ignore_missing: true + ignore_web_source_paths: [testdir/**/*.js] '''; final commandLog = await runWith(version, config); const release = '$name@$version'; @@ -103,7 +103,7 @@ void main() { '$cli $args debug-files upload $orgAndProject --include-sources $buildDir/app/outputs', '$cli $args releases $orgAndProject new $release', '$cli sourcemaps inject $buildDir/web/file.js $orgAndProject', - '$cli $args sourcemaps upload --release $release $buildDir/web --ext js --ext map --strip-prefix ../../Documents --strip-prefix ../../../../ --strip-prefix ../../ --strip-prefix ../ ./ --ext dart $orgAndProject', + '$cli $args sourcemaps upload --release $release $buildDir/web --ext js --ext map --strip-prefix ../../Documents --strip-prefix ../../../../ --strip-prefix ../../ --strip-prefix ../ ./ --ext dart --ignore testdir/**/*.js $orgAndProject', '$cli $args releases $orgAndProject set-commits $release --auto --ignore-missing', '$cli $args releases $orgAndProject finalize $release' ]); diff --git a/test/utils/config_formatter.dart b/test/utils/config_formatter.dart index 9e777e76..52db56f3 100644 --- a/test/utils/config_formatter.dart +++ b/test/utils/config_formatter.dart @@ -2,39 +2,88 @@ import 'config_file_type.dart'; class ConfigFormatter { static String formatConfig( - String config, ConfigFileType fileType, String? url) { - // Add URL if provided - if (url != null) { - config = _addUrlPrefix(config, fileType, url); + String config, + ConfigFileType fileType, + String? url, + ) { + if (url?.isNotEmpty == true) { + config = _addUrlPrefix(config, fileType, url!); } - // Format config based on file type switch (fileType) { case ConfigFileType.sentryProperties: return _formatSentryPropertiesConfig(config); case ConfigFileType.pubspecYaml: return _formatPubspecYamlConfig(config); - default: - throw Exception('Unknown config file type: $fileType'); } } static String _addUrlPrefix( - String config, ConfigFileType fileType, String url) { + String config, + ConfigFileType fileType, + String url, + ) { final urlLine = fileType == ConfigFileType.sentryProperties ? 'url=$url' : 'url: $url'; return '$urlLine\n$config'; } static String _formatSentryPropertiesConfig(String config) { - return config - .replaceAll(': ', '=') - .split('\n') - .map((line) => line.trim()) - .join('\n'); + final lines = config.split('\n'); + final out = StringBuffer(); + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line.isEmpty) continue; + + // 1) normalise key/value separator once + line = line.replaceFirstMapped( + RegExp(r'^([^=:\s]+):\s*'), (m) => '${m.group(1)}='); + + // 2) inline array + final mInline = RegExp(r'(.*)=\[(.*)\]').firstMatch(line); + if (mInline != null) { + final key = mInline.group(1)!.trim(); + final values = + mInline.group(2)!.split(',').map((v) => v.trim()).join(','); + out.writeln('$key=$values'); + continue; + } + + // 3) block-array start + if (RegExp(r'.*=').hasMatch(line) && line.endsWith('=')) { + final key = line.substring(0, line.length - 1).trim(); + final values = []; + + while (i + 1 < lines.length) { + final next = lines[i + 1].trim(); + if (next.startsWith('- ')) { + values.add(next.substring(2).trim()); + i++; // consume + } else if (next.isEmpty) { + i++; // skip blank + } else { + break; + } + } + + out.writeln('$key=${values.join(',')}'); + continue; + } + + // 4) plain key=value line + out.writeln(line); + } + + return out.toString().trimRight(); } static String _formatPubspecYamlConfig(String config) { - return config.split('\n').map((line) => ' ${line.trim()}').join('\n'); + return config + .split('\n') + .map((l) => + l.trim().startsWith('- ') ? ' ${l.trim()}' : ' ${l.trim()}') + .join('\n') + .trimRight(); } }