Skip to main content

Upload Flutter debug artifacts

Flutter has more than one symbol format. A native release can contain Dart AOT frames, iOS native frames, and Android Java/Kotlin or native frames. Those frames do not use JavaScript source maps and do not share one interchangeable symbol file. @aient/sourcemaps is an npm-distributed build-time CI tool. Aient uses npm to distribute one versioned, cross-platform command that can validate files and call the upload API from a trusted release runner. It is never linked into your Flutter application, added to pubspec.yaml, or run on a phone. Installing it does not inject Node.js or an Aient uploader into the app process. Flutter Web is the exception: it emits JavaScript and uses the regular source map upload flow.

What Aient accepts today

The current command accepts dart_aot_dwarf and apple_dsym manifest items. An Apple item is one thin, raw Mach-O DWARF member inside a dSYM bundle; the bundle directory, ZIP archives, and fat/universal objects are rejected. Android R8/ProGuard mappings and Android native ELF files are not accepted. Generate and retain every build-specific artifact because it generally cannot be recreated after a rebuild. An accepted storage commit proves that Aient retained the exact verified bytes; it does not by itself prove that a later occurrence was symbolicated.

Install the release tool

Keep Node release tooling separate from Flutter runtime dependencies. For example, check in this file as release-tooling/package.json:
Create and commit the matching lockfile, then use only the installed, lockfile-pinned binary in release CI:
With npm, the equivalent setup is:
Do not use an unpinned npx, pnpm dlx, branch URL, or latest tag in the authoritative release job. The committed manifest and lockfile are the supply chain record for the uploader version used by that release.

Keep telemetry and upload credentials separate

Publishable environment keys and the private upload credential are not interchangeable. A development build should use the development publishable key, automated app tests should use QA, staging builds should use staging, and TestFlight/store delivery builds should use production unless they are explicitly a staging distribution. None of those publishable values authorizes a symbol upload. Make one application-owned build intent the source of both the canonical Aient environment slug and its publishable key: Do not accept an independently selected slug and arbitrary syntactically valid publishable key. Resolve both from the build intent, including for development, and make a protected release fail if an explicit override does not equal the key configured for that intent. Publishable keys may be embedded in a mobile client; the upload-scoped AIENT_API_KEY must never be embedded. Configure AIENT_TELEMETRY_ENABLED as default-on for every build intent. Only the normalized values false and 0 should opt out. Omission, true, and malformed values must leave telemetry enabled so a missing CI-only define cannot silently remove release observability. In Aient, open Settings → API keys → Create Key and create a key with the upload scope. Copy the value when it is shown—the value is displayed only once—and store it in the protected CI secret used by your release workflow (the example below names that secret AIENT_DEBUG_ARTIFACT_UPLOAD_KEY). Do not grant admin scope, and do not substitute an app’s publishable ingest key. The uploader does not proactively print configuration or perform secret redaction. String failure messages are preserved verbatim. Structured non-string failure values use bounded Node.js inspection (depth 8, at most 100 array items, and at most 100,000 characters per string) so pathological objects cannot flood the runner. Keep credentials out of command arguments and user-supplied exception values; CI log hygiene remains the workflow owner’s responsibility.

Create one immutable release identity

Before the authoritative build, create one immutable AIENT_RELEASE_ID. Pass the same value to the app and to the manifest generated from that exact build. Also capture the exact lowercase 40-character commit SHA, app version, build number, platform, and flavor/variant. Set these resource attributes on Flutter exception telemetry: Commit SHA is provenance. Aient selects Dart debug artifacts by exact service, release ID, platform, and architecture, so a rebuild from the same commit needs a new release ID and its own artifacts.

Android: produce AAB, Dart symbols, and R8 mapping

Build the exact AAB that you will distribute. --obfuscate renames Dart symbols; --split-debug-info writes the matching per-architecture Dart debug files outside the AAB.
Remove --flavor and -t if the project has no flavors. A flavored project normally produces an AAB below build/app/outputs/bundle/<flavor>Release/; an unflavored Flutter project normally produces it below build/app/outputs/bundle/release/. Select one exact file and fail if zero or multiple candidates exist. The split-debug directory contains names such as:
Use Aient architecture x86_64 for Flutter’s x64 filename token. Include every architecture emitted by the distributed build in the manifest. Flutter deferred components can also emit loading-unit files such as app.android-arm64.symbols-2.part.so. Version 0.3.4 has no loading-unit identity field and permits only one Android dart_aot_dwarf item per architecture, so it can symbolicate only the root loading unit. Do not add loading-unit files to the current manifest. Retain every *.symbols-*.part.so file with the distributed build for manual recovery and future uploader support. Flutter release builds run Android’s R8 shrinker. If your Android Gradle build minifies Java/Kotlin, preserve the exact mapping produced for the variant:
Only enable this when minification is part of your tested release contract. The mapping is build-specific and is overwritten by a later build.
For example, a productionRelease variant normally writes build/app/outputs/mapping/productionRelease/mapping.txt. Confirm the real path in your Android Gradle Plugin version and fail the release if minification is enabled but the file is absent. Do not enable minification solely for Aient while Aient still cannot ingest the mapping; if you already enable it, retain the mapping for Google Play or retrace.

iOS: produce IPA, Dart symbols, and dSYMs

For every Release configuration used for distribution, set Xcode’s Debug Information Format (DEBUG_INFORMATION_FORMAT) to DWARF with dSYM File. Apple ties each binary to its dSYM by UUID, so a dSYM from a rebuild is not a substitute even when the source commit is unchanged. Build the exact archive and IPA that will be sent to TestFlight or the App Store:
The build produces four independently important outputs: flutter build ios is not the authoritative TestFlight artifact: use flutter build ipa so the release job owns both the .xcarchive and the exact exported IPA. Do not rebuild after manifest validation. Hash the IPA, upload the Dart symbols and selected dSYM DWARF members, and publish that already-built IPA.

Generate manifest v1

upload-debug validates and uploads a manifest; it intentionally does not guess release identity or generate one by scanning an arbitrary build directory. Check in a small release helper (Dart, Node.js, Ruby, or your CI language) that:
  1. finds every .symbols regular file in the exact split-debug directory and, for iOS, each selected raw DWARF member inside the archive’s dSYM bundles;
  2. maps Dart filenames to android|ios and arm|arm64|x86_64, and reads each Apple member’s UUID and arm64|x86_64 architecture from its Mach-O bytes;
  3. calculates SHA-256 and byte length for every debug file and the exact AAB or IPA;
  4. writes one strict snake_case manifest beside the symbol files; and
  5. fails on empty files, unknown names, duplicates, symlinks, or more than one selected distribution.
The current @aient/sourcemaps package does not ship a manifest generator. The tool/aient_debug_manifest.dart path in the CI example below is an app-owned, checked-in helper—not a command installed by Aient. Keep its implementation and contract tests in the mobile repository so project-specific flavors and output paths remain explicit. Do not replace it with an unsupported build-directory scan or an invented generate-debug-manifest CLI command. Every artifact_path is relative to the manifest directory. Placing the manifest beside a Dart symbol file lets that path be its basename; an Apple path may descend into a retained dSYM directory as shown below. The CLI rejects absolute, traversing, symlinked, non-regular, hash-mismatched, size-mismatched, wrong-format, and wrong-architecture inputs. Each debug artifact is limited to 100 MiB.
Create one manifest per platform/distribution. For iOS, use platform: "ios", the exact IPA basename and hash, and the app.ios-*.symbols files from the same build. Add each native member as a separate apple_dsym item with its canonical hyphenated Mach-O UUID in debug_id. artifact_path must point directly to the thin regular file; do not point it at the .dSYM directory or an archive:
The CLI inspects the bytes and rejects missing or mismatched UUIDs and architectures, archives, fat/universal objects, symlinks, and non-regular files.

Validate and upload

Run a local-only validation before using credentials:
Then upload from the protected release job:
The command exits non-zero for an empty manifest, local validation failure, service lookup failure, rejected upload, incomplete bulk response, storage failure, or commit failure. A success summary distinguishes artifacts accepted and committed to private storage from artifacts explicitly enqueued for processing. --wait is rejected because the current API does not expose a truthful per-artifact processing-completion receipt. Use this four-stage evidence ladder instead: 0 enqueued for processing means no durable processing admission was acknowledged by that command. It does not invalidate an accepted storage commit, but it is not a backfill or symbolication receipt. Until a truthful processing status API exists, only the controlled occurrence at stage 4 proves usable symbolication.

Retry without changing identity

After an ambiguous or transient upload failure, retry with the exact same manifest, symbol files, and distribution bytes. For the same service, release ID, artifact kind, platform, and architecture:
  • the same commit SHA, artifact SHA-256, and byte length is an idempotent replay; the server may classify it as already_committed, and the CLI counts it as accepted and committed;
  • a different commit SHA, artifact SHA-256, or byte length is an immutable identity conflict and is not allowed to overwrite the committed artifact; and
  • rebuilding, even from the same commit, is a new build rather than an upload retry. Give it a new release ID, regenerate the manifest and symbols, and publish only that rebuilt distribution.
A CI rerun that rebuilds with a new run-attempt release ID is therefore a new release candidate. A pure upload retry must reuse the retained outputs from the original build without rerunning Flutter.

Release-CI order

Use this order independently for Android and iOS:
Run uploads only in protected trusted release jobs. Pull requests and forks can run --dry-run against non-production fixtures, but must not receive AIENT_API_KEY or production configuration.

GitHub Actions skeleton

This Android example assumes tool/aient_debug_manifest.dart is your checked-in manifest helper implementing the five rules above. Keep the helper and uploader version under review like any other release tooling.
Use a separate macos job for iOS. Replace the build with the flutter build ipa command above, pass the one selected build/ios/ipa/*.ipa to the manifest and uploader, and retain build/debug-info/**, the complete build/ios/archive/Runner.xcarchive/**, and the exact build/ios/ipa/*.ipa before publishing that same IPA to App Store Connect. The full archive—not only its dSYM directory—is the authoritative recovery unit containing the matching binaries and UUID-bound dSYMs.

Troubleshooting

  • No .symbols files: both --obfuscate and --split-debug-info=<directory> must be on the release build command. Confirm the directory is unique to this release and platform.
  • Distribution mismatch: the file basename, SHA-256, and byte length must match distribution. Upload the exact file used to create the manifest and do not rebuild it.
  • Architecture mismatch: do not infer architecture from the runner. Map the Flutter filename, then let the CLI inspect the ELF or Mach-O bytes.
  • Artifact missing in Aient: confirm runtime service.name, aient.release.id, os.type, and host.arch exactly match the manifest.
  • Missing or rejected dSYM member: verify Release uses DWARF with dSYM File, inspect the archive’s dSYMs directory, and select the thin regular file under Contents/Resources/DWARF/ rather than the bundle directory or an archive.
  • Missing R8 mapping: verify whether the exact Gradle variant enables minification and inspect its outputs/mapping/<variant>/ directory. A mapping from another build cannot be substituted.
Primary platform references: Flutter obfuscation and split debug information, Flutter iOS release archives, Flutter Android release builds, Apple dSYM build information, and Android R8 mapping files.