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 asrelease-tooling/package.json:
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 immutableAIENT_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.
--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:
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:
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:
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:
- finds every
.symbolsregular file in the exact split-debug directory and, for iOS, each selected raw DWARF member inside the archive’s dSYM bundles; - maps Dart filenames to
android|iosandarm|arm64|x86_64, and reads each Apple member’s UUID andarm64|x86_64architecture from its Mach-O bytes; - calculates SHA-256 and byte length for every debug file and the exact AAB or IPA;
- writes one strict snake_case manifest beside the symbol files; and
- fails on empty files, unknown names, duplicates, symlinks, or more than one selected distribution.
@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.
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:
Validate and upload
Run a local-only validation before using credentials:--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.
Release-CI order
Use this order independently for Android and iOS:--dry-run against non-production fixtures, but must not receive
AIENT_API_KEY or production configuration.
GitHub Actions skeleton
This Android example assumestool/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.
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
.symbolsfiles: both--obfuscateand--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, andhost.archexactly match the manifest. - Missing or rejected dSYM member: verify Release uses DWARF with dSYM File,
inspect the archive’s
dSYMsdirectory, and select the thin regular file underContents/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.