Skip to content

Commit 0eb0b26

Browse files
authored
chore: flutter symbol collector CLI tool (#1673)
1 parent 891efac commit 0eb0b26

21 files changed

+917
-0
lines changed

.github/workflows/flutter-symbols.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Flutter symbols collection
2+
on:
3+
schedule:
4+
# Run once an hour. It takes just a couple of minutes because of status caching.
5+
- cron: "10 * * * *"
6+
workflow_dispatch:
7+
inputs:
8+
flutter_version:
9+
description: Flutter version, can be either a specific version (3.17.0) or a wildcard (3.2.*)
10+
required: false
11+
type: string
12+
default: "3.*.*"
13+
14+
defaults:
15+
run:
16+
working-directory: scripts/flutter_symbol_collector
17+
18+
jobs:
19+
test:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v3
23+
24+
- uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d # pin@v1
25+
26+
- run: dart pub get
27+
28+
- run: dart test
29+
30+
run:
31+
needs: [test]
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v3
35+
36+
- uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d # pin@v1
37+
38+
- run: dart pub get
39+
40+
- name: Download status cache of previously processed files
41+
run: |
42+
gh run download --name 'flutter-symbol-collector-database' --dir .cache
43+
grep -r "" .cache
44+
continue-on-error: true
45+
env:
46+
GITHUB_TOKEN: ${{ github.token }}
47+
48+
- run: dart run bin/flutter_symbol_collector.dart --version=${{ inputs.flutter_version || '3.*.*' }}
49+
timeout-minutes: 300
50+
env:
51+
GITHUB_TOKEN: ${{ github.token }}
52+
53+
- name: Upload updated status cache of processed files
54+
uses: actions/upload-artifact@v3
55+
if: always()
56+
with:
57+
name: flutter-symbol-collector-database
58+
path: scripts/flutter_symbol_collector/.cache

.github/workflows/update-deps.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ jobs:
3535
changelog-entry: false
3636
secrets:
3737
api-token: ${{ secrets.CI_DEPLOY_KEY }}
38+
39+
symbol-collector:
40+
uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
41+
with:
42+
path: scripts/update-symbol-collector.sh
43+
name: Symbol collector CLI
44+
changelog-entry: false
45+
secrets:
46+
api-token: ${{ secrets.CI_DEPLOY_KEY }}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Enhancements
66

77
- Log warning if both tracesSampleRate and tracesSampler are set ([#1701](https://github.com/getsentry/sentry-dart/pull/1701))
8+
- Better Flutter framework stack traces - we now collect Flutter framework debug symbols for iOS, macOS and Android automatically on the Sentry server ([#1673](https://github.com/getsentry/sentry-dart/pull/1673))
89

910
### Features
1011

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# https://dart.dev/guides/libraries/private-files
2+
# Created by `dart pub`
3+
.dart_tool/
4+
5+
.temp
6+
.cache
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Flutter symbol collector
2+
3+
This is an internal tool to collect Flutter debug symbols and upload them to Sentry.
4+
This application is not intended for public usage - we're uploading the symbols in CI automatically so you don't have to.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
include: package:lints/recommended.yaml
2+
3+
linter:
4+
rules:
5+
prefer_relative_imports: true
6+
unnecessary_brace_in_string_interps: true
7+
unawaited_futures: true
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:args/args.dart';
2+
import 'package:file/local.dart';
3+
import 'package:flutter_symbol_collector/flutter_symbol_collector.dart';
4+
import 'package:github/github.dart';
5+
import 'package:logging/logging.dart';
6+
7+
const githubToken = String.fromEnvironment('GITHUB_TOKEN');
8+
final githubAuth = githubToken.isEmpty
9+
? Authentication.anonymous()
10+
: Authentication.withToken(githubToken);
11+
final source = FlutterSymbolSource(githubAuth: githubAuth);
12+
final fs = LocalFileSystem();
13+
final tempDir = fs.currentDirectory.childDirectory('.temp');
14+
final stateCache =
15+
DirectoryStatusCache(fs.currentDirectory.childDirectory('.cache'));
16+
late final SymbolCollectorCli collector;
17+
18+
void main(List<String> arguments) async {
19+
Logger.root.level = Level.ALL;
20+
Logger.root.onRecord.listen((record) {
21+
print('${record.level.name}: ${record.time}: ${record.message}'
22+
'${record.error == null ? '' : ': ${record.error}'}');
23+
});
24+
25+
final parser = ArgParser()..addOption('version', defaultsTo: '');
26+
final args = parser.parse(arguments);
27+
final argVersion = args['version'] as String;
28+
29+
collector = await SymbolCollectorCli.setup(tempDir);
30+
31+
// If a specific version was given, run just for this version.
32+
if (argVersion.isNotEmpty &&
33+
!argVersion.contains('*') &&
34+
argVersion.split('.').length == 3) {
35+
Logger.root.info('Running for a single flutter version: $argVersion');
36+
await processFlutterVersion(FlutterVersion(argVersion));
37+
} else {
38+
// Otherwise, walk all the versions and run for the matching ones.
39+
final versionRegex = RegExp(argVersion.isEmpty
40+
? '.*'
41+
: '^${argVersion.replaceAll('.', '\\.').replaceAll('*', '.+')}\$');
42+
Logger.root.info('Running for all Flutter versions matching $versionRegex');
43+
final versions = await source
44+
.listFlutterVersions()
45+
.where((v) => !v.isPreRelease)
46+
.where((v) => versionRegex.hasMatch(v.tagName))
47+
.toList();
48+
Logger.root.info(
49+
'Found ${versions.length} Flutter versions matching $versionRegex');
50+
for (var version in versions) {
51+
await processFlutterVersion(version);
52+
}
53+
}
54+
}
55+
56+
Future<void> processFlutterVersion(FlutterVersion version) async {
57+
if (bool.hasEnvironment('CI')) {
58+
print('::group::Processing Flutter ${version.tagName}');
59+
}
60+
Logger.root.info('Processing Flutter ${version.tagName}');
61+
Logger.root.info('Engine version: ${await version.engineVersion}');
62+
63+
final archives = await source.listSymbolArchives(version);
64+
final dir = tempDir.childDirectory(version.tagName);
65+
for (final archive in archives) {
66+
final status = await stateCache.getStatus(archive);
67+
if (status == SymbolArchiveStatus.success) {
68+
Logger.root
69+
.info('Skipping ${archive.path} - already processed successfully');
70+
continue;
71+
}
72+
73+
final archiveDir = dir.childDirectory(archive.platform.operatingSystem);
74+
try {
75+
if (await source.downloadAndExtractTo(archiveDir, archive.path)) {
76+
if (await collector.upload(archiveDir, archive.platform, version)) {
77+
await stateCache.setStatus(archive, SymbolArchiveStatus.success);
78+
continue;
79+
}
80+
}
81+
await stateCache.setStatus(archive, SymbolArchiveStatus.error);
82+
} finally {
83+
if (await archiveDir.exists()) {
84+
await archiveDir.delete(recursive: true);
85+
}
86+
}
87+
}
88+
89+
if (await dir.exists()) {
90+
await dir.delete(recursive: true);
91+
}
92+
93+
if (bool.hasEnvironment('CI')) {
94+
print('::endgroup::');
95+
}
96+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export 'src/flutter_symbol_source.dart';
2+
export 'src/flutter_version.dart';
3+
export 'src/symbol_collector_cli.dart';
4+
export 'src/status_cache.dart';
5+
export 'src/symbol_archive.dart';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:gcloud/storage.dart';
2+
import 'package:platform/platform.dart';
3+
4+
import 'symbol_archive.dart';
5+
6+
abstract class FlutterSymbolResolver {
7+
final String _prefix;
8+
final Bucket _bucket;
9+
final _resolvedFiles = List<SymbolArchive>.empty(growable: true);
10+
Platform get platform;
11+
12+
FlutterSymbolResolver(this._bucket, String prefix)
13+
: _prefix = prefix.endsWith('/')
14+
? prefix.substring(0, prefix.length - 1)
15+
: prefix;
16+
17+
Future<void> tryResolve(String path) async {
18+
path = '$_prefix/$path';
19+
final matches = await _bucket
20+
.list(prefix: path)
21+
.where((v) => v.isObject)
22+
.where((v) => v.name == path) // because it's a prefix search
23+
.map((v) => v.name)
24+
.toList();
25+
if (matches.isNotEmpty) {
26+
_resolvedFiles.add(SymbolArchive(matches.single, platform));
27+
}
28+
}
29+
30+
Future<List<SymbolArchive>> listArchives();
31+
}
32+
33+
class IosSymbolResolver extends FlutterSymbolResolver {
34+
IosSymbolResolver(super.bucket, super.prefix);
35+
36+
@override
37+
final platform = FakePlatform(operatingSystem: Platform.iOS);
38+
39+
@override
40+
Future<List<SymbolArchive>> listArchives() async {
41+
await tryResolve('ios-release/Flutter.dSYM.zip');
42+
return _resolvedFiles;
43+
}
44+
}
45+
46+
class MacOSSymbolResolver extends FlutterSymbolResolver {
47+
MacOSSymbolResolver(super.bucket, super.prefix);
48+
49+
@override
50+
final platform = FakePlatform(operatingSystem: Platform.macOS);
51+
52+
@override
53+
Future<List<SymbolArchive>> listArchives() async {
54+
// darwin-x64-release directory contains a fat (arm64+x86_64) binary.
55+
await tryResolve('darwin-x64-release/FlutterMacOS.dSYM.zip');
56+
return _resolvedFiles;
57+
}
58+
}
59+
60+
class AndroidSymbolResolver extends FlutterSymbolResolver {
61+
final String architecture;
62+
63+
AndroidSymbolResolver(super.bucket, super.prefix, this.architecture);
64+
65+
@override
66+
final platform = FakePlatform(operatingSystem: Platform.android);
67+
68+
@override
69+
Future<List<SymbolArchive>> listArchives() async {
70+
await tryResolve('android-$architecture-release/symbols.zip');
71+
return _resolvedFiles;
72+
}
73+
}

0 commit comments

Comments
 (0)