From f28972e666d07c3008b99a7917f31c7740c188e7 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:37:08 +0200 Subject: [PATCH 1/2] feat(crash-reporting): wire Sentry into release builds --- .github/workflows/release.yaml | 28 +++++++--- android/fastlane/Fastfile | 94 ++++++++++++++++++++++++++-------- ios/fastlane/Fastfile | 60 +++++++++++++++++++--- pubspec.lock | 48 +++++++++++++++++ pubspec.yaml | 12 +++++ 5 files changed, 208 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8732c0366..7fdbe341a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -39,6 +39,7 @@ jobs: outputs: proceed: ${{ steps.check.outputs.proceed }} is_prerelease: ${{ steps.check.outputs.is_prerelease }} + sentry_environment: ${{ steps.check.outputs.sentry_environment }} steps: - name: Inspect tag id: check @@ -48,19 +49,28 @@ jobs: # Strict shape: vX.Y.Z with all parts numeric, no suffix. if ! echo "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::notice::Tag $TAG is not a plain vX.Y.Z tag; skipping release." - echo "proceed=false" >> $GITHUB_OUTPUT - echo "is_prerelease=false" >> $GITHUB_OUTPUT + { + echo "proceed=false" + echo "is_prerelease=false" + echo "sentry_environment=internal" + } >> "$GITHUB_OUTPUT" exit 0 fi PATCH=$(echo "$TAG" | cut -d. -f3) if [ "$PATCH" -eq 0 ]; then echo "::notice::Tag $TAG has PATCH=0; running production-candidate release." - echo "proceed=true" >> $GITHUB_OUTPUT - echo "is_prerelease=false" >> $GITHUB_OUTPUT + { + echo "proceed=true" + echo "is_prerelease=false" + echo "sentry_environment=production" + } >> "$GITHUB_OUTPUT" else echo "::notice::Tag $TAG has PATCH=$PATCH; running internal release." - echo "proceed=true" >> $GITHUB_OUTPUT - echo "is_prerelease=true" >> $GITHUB_OUTPUT + { + echo "proceed=true" + echo "is_prerelease=true" + echo "sentry_environment=internal" + } >> "$GITHUB_OUTPUT" fi store-metadata-preflight: @@ -172,6 +182,9 @@ jobs: working-directory: android env: NEW_VERSION: ${{ github.ref_name }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_ENVIRONMENT: ${{ needs.guard.outputs.sentry_environment }} - name: Upload APK artifact uses: actions/upload-artifact@v4 @@ -269,6 +282,9 @@ jobs: working-directory: ios env: NEW_VERSION: ${{ github.ref_name }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_ENVIRONMENT: ${{ needs.guard.outputs.sentry_environment }} - name: Upload IPA artifact uses: actions/upload-artifact@v4 diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 93593b8eb..946ac0125 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -13,8 +13,41 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane +require "json" +require "tempfile" + default_platform(:android) +# Keeps the DSN out of the process arguments Fastlane prints. Flutter reads +# both compile-time values from this short-lived, mode-0600 JSON file. +def with_crash_reporting_defines + dsn = ENV.fetch("SENTRY_DSN", "") + UI.user_error!("SENTRY_DSN is required for store release builds") if dsn.empty? + + environment = ENV.fetch("SENTRY_ENVIRONMENT", "production") + file = Tempfile.new(["sentry-defines", ".json"]) + File.chmod(0o600, file.path) + file.write(JSON.generate({ + "SENTRY_DSN" => dsn, + "SENTRY_ENVIRONMENT" => environment, + })) + file.flush + yield file.path +ensure + file&.close! +end + +def upload_sentry_symbols(marketing_version:, version_code:, symbols_path:, dart_symbol_map_path:) + ENV["SENTRY_RELEASE"] = "swiss.realunit.app@#{marketing_version}+#{version_code}" + ENV["SENTRY_DIST"] = version_code.to_s + Dir.chdir("../..") do + sh("dart", "run", "sentry_dart_plugin", + "--sentry-define=symbols_path=#{symbols_path}", + "--sentry-define=dart_symbol_map_path=#{dart_symbol_map_path}" + ) + end +end + # Runs tool/generate_release_info.dart (single source of truth for tag → # marketing version + version code) and reads the values back from the # generated Dart file. The generated file is the canonical artefact — @@ -61,29 +94,48 @@ platform :android do marketing_version = info["marketing_version"] version_code = info["version_code"] - Dir.chdir("..") do - sh("flutter", "build", "appbundle", - "--release", - "--build-name=#{marketing_version}", - "--build-number=#{version_code}", - "--no-tree-shake-icons", - "--obfuscate", - "--split-debug-info=build/debug_info" - ) - sh("flutter", "build", "apk", - "--release", - "--build-name=#{marketing_version}", - "--build-number=#{version_code}", - "--no-tree-shake-icons", - "--obfuscate", - "--split-debug-info=build/debug_info" - ) - File.rename( - "../build/app/outputs/flutter-apk/app-release.apk", - "../build/app/outputs/flutter-apk/realunit-#{tag_name}.apk" - ) + with_crash_reporting_defines do |defines_file| + Dir.chdir("..") do + sh("flutter", "build", "appbundle", + "--release", + "--build-name=#{marketing_version}", + "--build-number=#{version_code}", + "--dart-define-from-file=#{defines_file}", + "--no-tree-shake-icons", + "--obfuscate", + "--split-debug-info=build/debug_info/appbundle", + "--extra-gen-snapshot-options=--save-obfuscation-map=build/appbundle-obfuscation-map.json" + ) + sh("flutter", "build", "apk", + "--release", + "--build-name=#{marketing_version}", + "--build-number=#{version_code}", + "--dart-define-from-file=#{defines_file}", + "--no-tree-shake-icons", + "--obfuscate", + "--split-debug-info=build/debug_info/apk", + "--extra-gen-snapshot-options=--save-obfuscation-map=build/apk-obfuscation-map.json" + ) + File.rename( + "../build/app/outputs/flutter-apk/app-release.apk", + "../build/app/outputs/flutter-apk/realunit-#{tag_name}.apk" + ) + end end + upload_sentry_symbols( + marketing_version: marketing_version, + version_code: version_code, + symbols_path: "build/debug_info/appbundle", + dart_symbol_map_path: "build/appbundle-obfuscation-map.json" + ) + upload_sentry_symbols( + marketing_version: marketing_version, + version_code: version_code, + symbols_path: "build/debug_info/apk", + dart_symbol_map_path: "build/apk-obfuscation-map.json" + ) + # Upload the binary + changelog only. The changelog is tied to this # build's version code (taken from the AAB); the listing metadata and # images are pushed by the dedicated call below, keeping the two diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 80d96da79..05ea96ac2 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -13,8 +13,38 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane +require "json" +require "tempfile" + default_platform(:ios) +# Keeps the DSN out of the process arguments Fastlane prints. Flutter writes +# the encoded defines into Generated.xcconfig before gym invokes xcodebuild. +def with_crash_reporting_defines + dsn = ENV.fetch("SENTRY_DSN", "") + UI.user_error!("SENTRY_DSN is required for store release builds") if dsn.empty? + + environment = ENV.fetch("SENTRY_ENVIRONMENT", "production") + file = Tempfile.new(["sentry-defines", ".json"]) + File.chmod(0o600, file.path) + file.write(JSON.generate({ + "SENTRY_DSN" => dsn, + "SENTRY_ENVIRONMENT" => environment, + })) + file.flush + yield file.path +ensure + file&.close! +end + +def upload_sentry_symbols(marketing_version:, version_code:) + ENV["SENTRY_RELEASE"] = "swiss.realunit.app@#{marketing_version}+#{version_code}" + ENV["SENTRY_DIST"] = version_code.to_s + Dir.chdir("../..") do + sh("dart", "run", "sentry_dart_plugin") + end +end + def get_api_key app_store_connect_api_key( key_id: ENV['FASTLANE_APPLE_API_KEY_ID'], @@ -110,13 +140,29 @@ platform :ios do xcodeproj: "Runner.xcodeproj" ) - gym( - workspace: "Runner.xcworkspace", - scheme: "Runner", - output_name: "realunit-#{tag_name}.ipa", - xcargs: "-verbose", - verbose: true - ) + with_crash_reporting_defines do |defines_file| + Dir.chdir("../..") do + sh("flutter", "build", "ios", + "--config-only", + "--release", + "--build-name=#{marketing_version}", + "--build-number=#{version_code}", + "--dart-define-from-file=#{defines_file}" + ) + end + gym( + workspace: "Runner.xcworkspace", + scheme: "Runner", + archive_path: File.expand_path("../../build/ios/archive/Runner.xcarchive", __dir__), + output_name: "realunit-#{tag_name}.ipa", + xcargs: "-verbose", + verbose: true + ) + upload_sentry_symbols( + marketing_version: marketing_version, + version_code: version_code + ) + end upload_to_testflight( api_key: get_api_key, diff --git a/pubspec.lock b/pubspec.lock index 759158cf0..a6d39b085 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -655,6 +655,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + globbing: + dependency: transitive + description: + name: globbing + sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d" + url: "https://pub.dev" + source: hosted + version: "1.0.0" go_router: dependency: "direct main" description: @@ -791,6 +799,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + injector: + dependency: transitive + description: + name: injector + sha256: ed389bed5b48a699d5b9561c985023d0d5cc88dd5ff2237aadcce5a5ab433e4e + url: "https://pub.dev" + source: hosted + version: "3.0.0" intl: dependency: "direct main" description: @@ -1183,6 +1199,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + properties: + dependency: transitive + description: + name: properties + sha256: "333f427dd4ed07bdbe8c75b9ff864a1e70b5d7a8426a2e8bdd457b65ae5ac598" + url: "https://pub.dev" + source: hosted + version: "2.1.1" provider: dependency: transitive description: @@ -1239,6 +1271,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + sentry_dart_plugin: + dependency: "direct dev" + description: + name: sentry_dart_plugin + sha256: da9c1d0b3c87a251bfc36301f16af090a88c2d59128fe9a6f908f5ac20340c97 + url: "https://pub.dev" + source: hosted + version: "3.4.0" shared_preferences: dependency: "direct main" description: @@ -1412,6 +1452,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + system_info2: + dependency: transitive + description: + name: system_info2 + sha256: b937736ecfa63c45b10dde1ceb6bb30e5c0c340e14c441df024150679d65ac43 + url: "https://pub.dev" + source: hosted + version: "4.1.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c6634fa58..0a22ecbc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -100,6 +100,7 @@ dev_dependencies: # Used by test/packages/config/legal_documents_config_test.dart to swap in a # recording fake for url_launcher without touching the platform channel. plugin_platform_interface: ^2.1.8 + sentry_dart_plugin: 3.4.0 url_launcher_platform_interface: ^2.3.2 dependency_overrides: @@ -111,6 +112,17 @@ dependency_overrides: url: https://github.com/cake-tech/web3dart.git ref: aa3f932dbf54eda651b7bd01ad00204acf998bd0 +sentry: + upload_debug_symbols: true + upload_source_maps: false + upload_sources: false + project: realunitchapp + org: sentry + url: https://sentry.dfxserve.com/ + wait_for_processing: true + log_level: error + commits: false + hooks: user_defines: sqlite3: From 1829cd04f9c54c37ba73dc536ce51fb86ec1ff8a Mon Sep 17 00:00:00 2001 From: Daniel Padrino Date: Fri, 24 Jul 2026 16:13:27 -0300 Subject: [PATCH 2/2] fix(release): fail loud when Sentry symbols/map are missing The plugin only warns and continues when native debug files or the Dart obfuscation map are absent, which would ship a release with silently degraded crash symbolication. --- android/fastlane/Fastfile | 10 ++++++++++ ios/fastlane/Fastfile | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 946ac0125..eb2a22c02 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -41,6 +41,16 @@ def upload_sentry_symbols(marketing_version:, version_code:, symbols_path:, dart ENV["SENTRY_RELEASE"] = "swiss.realunit.app@#{marketing_version}+#{version_code}" ENV["SENTRY_DIST"] = version_code.to_s Dir.chdir("../..") do + # The plugin only warns and continues on missing input, which would ship + # a release with degraded (or no) crash symbolication silently. Fail the + # build instead. + if Dir.glob("#{symbols_path}/**/*").select { |f| File.file?(f) }.empty? + UI.user_error!("Sentry upload: no native debug symbols found under #{symbols_path}") + end + unless File.exist?(dart_symbol_map_path) + UI.user_error!("Sentry upload: Dart obfuscation map not found at #{dart_symbol_map_path}") + end + sh("dart", "run", "sentry_dart_plugin", "--sentry-define=symbols_path=#{symbols_path}", "--sentry-define=dart_symbol_map_path=#{dart_symbol_map_path}" diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 05ea96ac2..63eae30cc 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -41,6 +41,13 @@ def upload_sentry_symbols(marketing_version:, version_code:) ENV["SENTRY_RELEASE"] = "swiss.realunit.app@#{marketing_version}+#{version_code}" ENV["SENTRY_DIST"] = version_code.to_s Dir.chdir("../..") do + # The plugin only warns and continues on missing input, which would ship + # a release with degraded (or no) crash symbolication silently. Fail the + # build instead. + if Dir.glob("build/ios/archive/**/*.dSYM").empty? + UI.user_error!("Sentry upload: no dSYMs found under build/ios/archive") + end + sh("dart", "run", "sentry_dart_plugin") end end