@newinstance/bugwatch-cli
v0.1.3
Published
BugWatch CLI — upload Apple dSYM debug symbols for native crash symbolication.
Downloads
395
Readme
@newinstance/bugwatch-cli
Upload debug symbols and symbolication artifacts to BugWatch so crashes arrive symbolicated — function names, file paths, and line numbers instead of raw memory addresses and minified frames. CI-friendly, runs anywhere Node 18+ is available.
# Binary debug-symbol archives (Apple dSYM; Android native .so pre-zipped)
npx @newinstance/bugwatch-cli symbols upload MyApp.xcarchive \
--release 1.4.2 --build-number 318
# Text artifacts (R8/ProGuard mapping, JS source map, Dart symbols)
npx @newinstance/bugwatch-cli artifacts upload mapping.txt \
--release 1.4.2 --platform android --type r8Which command do I need?
The two commands are not interchangeable — they validate different things and feed different parts of the symbolication pipeline.
| You have | Command | Notes |
|---|---|---|
| .dSYM bundle, .xcarchive, dir of dSYMs, symbols.zip | symbols upload | Apple platforms. Matched by the Mach-O debug UUID. |
| Android native .so / .debug (ELF), zipped first | symbols upload native-symbols.zip --platform android | Matched by GNU build-id. Bare .so files and .so directories are rejected; only a pre-made .zip is accepted (directory scanning is dSYM-only). |
| R8 / ProGuard mapping.txt | artifacts upload --type r8 | Text file, not an archive. |
| JS / React Native source map (.map) | artifacts upload --type sourcemap | |
| Flutter Dart --split-debug-info symbols | artifacts upload --type dart-symbols | |
symbols upload accepts binary archives only and verifies magic bytes — passing
it a mapping.txt or a .map is rejected as UNRECOGNIZED_ARCHIVE.
Requirements
- Node.js 18 or later (ESM-native package)
- A macOS machine or macOS CI runner — dSYM files are produced by Xcode
- A BugWatch API key with the
symbols:uploadpermission scope (see Getting an API key)
Install
No install needed — run it on demand with npx (requires Node 18+):
npx @newinstance/bugwatch-cli symbols upload <path> ...Or install it globally — the bin name is bugwatch, so this exposes bugwatch on $PATH:
npm install -g @newinstance/bugwatch-cli
bugwatch symbols upload <path> ...Build from source (contributors / unreleased revisions)
To run a local build (e.g. an unreleased change, or an air-gapped runner without npm access):
# from the repo root
cd bugwatch-cli
npm install # or: bun install
npm run build # outputs dist/ + bin/bugwatch.mjs
node bin/bugwatch.mjs symbols upload <path> ...Getting an API key
- Open the merchant dashboard for your organisation.
- Navigate to API Keys (project-level or org-level depending on your plan).
- Create a secret key (
sk_test_…/sk_live_…). - Make sure the key has the
symbols:uploadpermission scope assigned. The key validator rejects requests from keys that lack this scope. - Copy the full value — it is shown only once. Store it as a CI secret (never hard-code it or commit it to source control).
The key is sent on every request as the x-api-key: <keyId>:<secret> header.
It is never placed in the URL or logged; the CLI prints a redacted form
(sk_l…3456) for human confirmation only.
Authentication
| Method | How |
|---|---|
| CLI flag | --token <keyId>:<secret> |
| Environment variable | BUGWATCH_AUTH_TOKEN=<keyId>:<secret> |
The environment variable is preferred for CI — set it as a secret in your pipeline so it is not visible in build logs.
The API endpoint can similarly be overridden via --api-url or
BUGWATCH_API_URL (default: https://api.newinstance.cloud).
Command reference
bugwatch symbols upload <path> [options]<path> is the only required positional argument. All metadata flags are optional
but recommended — the debug UUID embedded in every dSYM is the authoritative
match criterion; metadata improves search and display in the dashboard.
Flag reference
| Flag | Env | Type | Default | Description |
|---|---|---|---|---|
| --token | BUGWATCH_AUTH_TOKEN | string | — | Required. API secret key (keyId:secret). Never ship to a browser or app binary. |
| --api-url | BUGWATCH_API_URL | string | https://api.newinstance.cloud | BugWatch REST API base URL. |
| --platform | — | enum | ios | Target platform: ios · macos · tvos · watchos · visionos · catalyst |
| --release | — | string | — | Marketing version (e.g. 1.4.2). Improves dashboard discoverability. |
| --build-number | — | string | — | Build number (e.g. 318). Used to disambiguate the same version string across builds. |
| --bundle-id | — | string | — | App bundle identifier (e.g. com.example.MyApp). |
| --distribution | — | enum | — | app-store · testflight · adhoc · enterprise · development |
| --environment | — | string | — | Environment tag (e.g. production, staging). |
| --commit | — | string | — | Source commit SHA. Links crash reports to a specific code revision. |
| --upload-source | — | string | cli | Tag identifying the upload origin (e.g. ci, fastlane, run-script). Visible in the dashboard. |
Accepted input formats
| Input | Behaviour |
|---|---|
| MyApp.app.dSYM | Single dSYM bundle — packed into a zip and uploaded. |
| build/dSYMs/ (directory) | All *.dSYM bundles found recursively (max depth 4) are packed together into one zip. |
| MyApp.xcarchive | The dSYMs/ subdirectory of the archive is walked; all discovered bundles are packed. |
| symbols.zip | Uploaded as-is (packed dSYM bundles, or zipped Android ELF .so files with --platform android). |
| libfoo.so or a directory of .so files | Rejected (unsupported input / no .dSYM bundles found). Zip the tree first: (cd path/to/libs && zip -r native-symbols.zip .). |
artifacts upload
bugwatch artifacts upload <path> --release <v> --platform <p> --type <t>For text symbolication artifacts: R8/ProGuard mappings, JS source maps, and Flutter
Dart symbols. Unlike symbols upload, nothing is packed — the file is streamed as-is.
| Flag | Env | Type | Default | Description |
|---|---|---|---|---|
| --release | — | string | — | Required. Build/release identifier. Must match the release your SDK reports, or frames will not resolve. |
| --platform | — | enum | — | Required. android · ios · react-native · flutter |
| --type | — | enum | — | Required. proguard · r8 · dsym · sourcemap · dart-symbols |
| --build-id | — | string | — | Optional build identifier. |
| --token | BUGWATCH_AUTH_TOKEN | string | — | Required. API secret key (keyId:secret). Never ship to a browser or app binary. |
| --api-url | BUGWATCH_API_URL | string | https://api.newinstance.cloud | BugWatch REST API base URL. |
Uploads are idempotent per (release, platform, type) — re-uploading replaces the
previous artifact and deletes the superseded file.
# React Native / Expo source map
npx @newinstance/bugwatch-cli artifacts upload \
dist/_expo/static/js/ios/index-*.hbc.map \
--release "1.4.2+318" --platform ios --type sourcemap
# Android R8 mapping
npx @newinstance/bugwatch-cli artifacts upload \
app/build/outputs/mapping/release/mapping.txt \
--release 1.4.2 --platform android --type r8
# Flutter Dart obfuscation symbols
npx @newinstance/bugwatch-cli artifacts upload build/symbols.zip \
--release 1.4.2 --platform flutter --type dart-symbolsPack-and-retry behaviour
- Multiple dSYM bundles are zipped into a single
symbols.zipbefore upload, preserving each bundle's internal directory tree. - Each step is retried up to 3 times on transient failures (HTTP 5xx, HTTP 429, network errors), with 500 ms / 1 000 ms / 1 500 ms back-off between attempts. A failed transfer is retried without re-presigning, and the file stream is re-opened for each attempt.
- HTTP 4xx errors (auth failure, validation error) are not retried.
- The temp zip is always cleaned up on exit.
- If the server already holds an archive with the same sha256, it reports a duplicate and the transfer is skipped entirely.
How the upload works
Both commands use the same three-step flow. The file itself never passes through the BugWatch API — it is streamed directly to object storage, so uploads are not bound by request-size limits and memory stays flat regardless of file size (a 2 GiB archive uses the same memory as a 200 MiB one).
1. POST /api/v1/bugwatch/{debug-symbols,artifacts}/presign → { uploadId, uploadUrl }
2. PUT <uploadUrl> → the bytes, streamed to storage
3. POST /api/v1/bugwatch/{…}/uploads/{uploadId}/complete → verified, then queuedThe API key is sent on steps 1 and 3 only — never to the storage host, which is authorized by the presigned URL's own signature.
Verification. The CLI declares the file's size and sha256 at step 1. Step 3
confirms the object actually landed and that its size matches. For debug symbols the
worker then re-hashes the real bytes before indexing anything, and rejects a mismatch
(CHECKSUM_MISMATCH) — a declared checksum is never trusted on its own.
Verifying the upload
After a successful upload the CLI prints the uploadId:
✓ uploaded: ul_abc123 [QUEUED]You can poll the REST API to confirm indexing is complete:
curl -H "x-api-key: $BUGWATCH_AUTH_TOKEN" \
"https://api.newinstance.cloud/api/v1/bugwatch/debug-symbols/uploads/ul_abc123"status transitions: QUEUED → PROCESSING → AVAILABLE (ready to symbolicate) or
FAILED.
You can also list recent uploads:
curl -H "x-api-key: $BUGWATCH_AUTH_TOKEN" \
"https://api.newinstance.cloud/api/v1/bugwatch/debug-symbols/uploads?limit=10&status=AVAILABLE"Both endpoints require the symbols:read permission scope on the key.
CI integration recipes
Every recipe follows the same four steps:
- Build / archive the app so Xcode produces dSYMs (set Debug Information Format = DWARF with dSYM File for the Release config).
- Expose
BUGWATCH_AUTH_TOKENas a secret env var in the pipeline. - Run
npx @newinstance/bugwatch-cli symbols upload <path> --release <v> --build-number <n>. - Optionally confirm indexing via the REST API or dashboard.
Only the provider-specific wiring differs — see below.
Xcode Run Script build phase
Add a Run Script phase in Xcode (after "Compile Sources"). Set it to run for Release builds only. Xcode provides the dSYM path via a built-in env var.
if [ "$CONFIGURATION" != "Release" ] || [ "$PLATFORM_NAME" = "iphonesimulator" ]; then
exit 0
fi
[ -z "$BUGWATCH_AUTH_TOKEN" ] && {
echo "warning: BUGWATCH_AUTH_TOKEN unset — skipping dSYM upload"
exit 0
}
npx @newinstance/bugwatch-cli symbols upload "$DWARF_DSYM_FOLDER_PATH" \
--release "$MARKETING_VERSION" \
--build-number "$CURRENT_PROJECT_VERSION" \
--bundle-id "$PRODUCT_BUNDLE_IDENTIFIER" \
--upload-source run-scriptStore BUGWATCH_AUTH_TOKEN in Xcode build settings or a non-committed .xcconfig
file — not hard-coded in the script.
Local Xcode (manual archive)
To publish dSYMs from a build you archived on your own Mac (Xcode Product → Archive) — handy for a one-off symbolication fix without wiring up CI:
- Open Window → Organizer → Archives, right-click your archive → Show in Finder.
- Drag the
.xcarchiveinto Terminal to paste its exact path (Release archives contain adSYMs/folder; the CLI walks it for you).
export BUGWATCH_AUTH_TOKEN="<keyId>:<secret>"
npx @newinstance/bugwatch-cli symbols upload \
"~/Library/Developer/Xcode/Archives/2026-06-28/MyApp 2026-06-28 10.30.xcarchive" \
--release 1.4.2 \
--build-number 318 \
--bundle-id com.example.MyApp \
--upload-source local-xcodeThis is the same upload the CI recipes run, just triggered by hand. (You can also point
it at the exported dSYMs/ directory or a single MyApp.app.dSYM.)
Fastlane
After build_app / gym, pass the archive path to the CLI:
build_app(scheme: "MyApp", export_method: "app-store")
sh(
"npx @newinstance/bugwatch-cli symbols upload " \
"'#{lane_context[SharedValues::XCODEBUILD_ARCHIVE]}' " \
"--release '#{get_version_number}' " \
"--build-number '#{get_build_number}' " \
"--upload-source fastlane"
)Set BUGWATCH_AUTH_TOKEN in your Fastfile environment, a .env file, or your
CI's secret store.
GitHub Actions (macOS runner)
jobs:
ios-build:
runs-on: macos-14
env:
BUGWATCH_AUTH_TOKEN: ${{ secrets.BUGWATCH_AUTH_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Archive
run: |
xcodebuild \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp.xcarchive \
archive
- name: Upload dSYMs to BugWatch
run: |
npx @newinstance/bugwatch-cli symbols upload build/MyApp.xcarchive \
--release "$(agvtool mvers -terse1)" \
--build-number "${{ github.run_number }}" \
--upload-source ciAdd BUGWATCH_AUTH_TOKEN in Settings → Secrets and variables → Actions.
Xcode Cloud
Add a ci_post_xcodebuild.sh script to the root of your repo. Xcode Cloud sets
$CI_ARCHIVE_PATH to the .xcarchive produced by the workflow.
#!/bin/sh
set -e
[ -z "$BUGWATCH_AUTH_TOKEN" ] && exit 0
npx @newinstance/bugwatch-cli symbols upload "$CI_ARCHIVE_PATH" \
--release "$CI_PRODUCT_VERSION" \
--build-number "$CI_BUILD_NUMBER" \
--upload-source xcode-cloudSet BUGWATCH_AUTH_TOKEN in Xcode Cloud → Workflows → Environment → Environment
Variables (mark it Secret).
Bitrise
Add a Script step after the Xcode Archive & Export for iOS step.
Bitrise provides $BITRISE_XCARCHIVE_PATH (the archive) and
$BITRISE_DSYM_DIR_PATH (extracted dSYMs — either works).
npx @newinstance/bugwatch-cli symbols upload "$BITRISE_XCARCHIVE_PATH" \
--release "$BITRISE_APP_VERSION" \
--build-number "$BITRISE_BUILD_NUMBER" \
--upload-source bitriseAdd BUGWATCH_AUTH_TOKEN as a Secret in Bitrise Secrets.
Codemagic
Add a post-build script in your codemagic.yaml:
scripts:
- name: Upload dSYMs to BugWatch
script: |
npx @newinstance/bugwatch-cli symbols upload "$CM_EXPORT_DIR" \
--release "$CM_APP_VERSION" \
--build-number "$CM_BUILD_NUMBER" \
--upload-source codemagic
environment:
BUGWATCH_AUTH_TOKEN: $BUGWATCH_AUTH_TOKENStore BUGWATCH_AUTH_TOKEN in Codemagic → Team / App → Environment variables
(mark it Secret / Secured).
Other macOS CI (GitLab, CircleCI, Jenkins, generic shell)
The command is the same everywhere. The only differences are how you expose the archive path and secret:
- GitLab CI (macOS runner): add a job script step; set
BUGWATCH_AUTH_TOKENas a masked CI/CD variable in Settings → CI/CD → Variables. - CircleCI (macOS executor): add a
runstep; store the token in a Context or project environment variable. - Jenkins (macOS agent): add a shell build step; bind the token from a
Credentials (Secret Text) entry to the
BUGWATCH_AUTH_TOKENenv var. - Any macOS shell:
BUGWATCH_AUTH_TOKEN=<keyId>:<secret> \ npx @newinstance/bugwatch-cli symbols upload <archive> \ --release 1.4.2 --build-number 318
Deployment reporting (deploy)
The CLI also reports CI deployments so BugWatch can mark releases, catch failed deploys and
tag logs by deployment (requires the deploy:write scope):
bugwatch deploy start --release 1.4.2 # creates the deployment, prints/persists its id
bugwatch deploy run --stage build -- npm run build # wraps a command as a named stage
bugwatch deploy run --stage migrate -- npm run migrate
bugwatch deploy finish # marks success
bugwatch deploy fail --summary "migrations failed" # or marks failure- The deployment id resolves from
--id, thenBUGWATCH_DEPLOY_ID, then the.bugwatch-deployfiledeploy startwrites; on GitHub Actions it is also appended to$GITHUB_ENV. deploy runrequires--stageand streams the wrapped command's output as deploy logs; the command's exit code is preserved. Without a token or deploy id it warns and runs the command unreported rather than blocking your pipeline.deploy faildefaults--summarytodeployment failed.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Upload accepted by the server. |
| 1 | Usage or input error (missing path, no token, unsupported file type). |
| 2 | Upload failure (all retries exhausted or server rejected the request). |
Troubleshooting
No dSYM file generated
Confirm the scheme's Release configuration has Build Settings → Debug Information
Format = DWARF with dSYM File. Simulator builds never produce dSYMs — the guard
[ "$PLATFORM_NAME" = "iphonesimulator" ] in the run-script recipe handles this.
UUID mismatch — crash still not symbolicated
The dSYM must come from the exact same build that crashed. Re-upload the dSYM from
that specific archive. Compare UUIDs with dwarfdump --uuid MyApp.app.dSYM.
Missing framework symbols (React Native / Hermes / Flutter / third-party)
Upload the full .xcarchive or the dSYMs/ directory — not just your app's
.dSYM. Each bundled framework ships its own dSYM.
Duplicate upload
The CLI reports (duplicate of an existing upload) and exits 0. Nothing breaks;
the server de-duplicates by UUID.
Archive too large or rejected
Confirm the input is a real .dSYM, .xcarchive, directory, or .zip. The server
enforces a 2 GB size cap on dSYM uploads.
Authentication failure (HTTP 401)
The x-api-key value is wrong, expired, or the key does not have the
symbols:upload scope. Regenerate the key in the dashboard and confirm the scope
is assigned.
HTTP 429 / rate-limited The CLI retries automatically with back-off. If it still fails, wait a moment and re-run — or contact support.
Apple system framework frames unresolved Expected. Your uploaded dSYMs only contain symbols for your app and bundled frameworks. Apple's private OS symbols are resolved separately (or not at all for private-framework frames).
React Native JS or Flutter Dart frames unresolved
Those require source-map or --split-debug-info symbol uploads, which are separate
from dSYMs and handled by a different upload flow.
Links
- BugWatch dashboard: merchant dashboard → your project → Issues / Logs
- REST API Swagger:
https://api.newinstance.cloud/swagger.json - Upload status endpoint:
GET /api/v1/bugwatch/debug-symbols/uploads/{uploadId} - iOS SDK: bug-watch-ios
- Android SDK:
cloud.newinstance:bugwatchon Maven Central - JS/TS SDK:
@newinstance/bugwatchon npm
Package status
| Field | Value |
|---|---|
| Package name | @newinstance/bugwatch-cli |
| Version | 0.1.2 |
| npm published | Yes — 0.1.2 on npm |
| Tests | bun test — 17 passing (discovery, upload retry/redaction, CLI validation) |
| Platforms supported | Apple dSYM (ios, macos, tvos, watchos, visionos, catalyst) plus pre-zipped Android ELF (--platform android); text artifacts via artifacts upload (android, ios, react-native, flutter) |
| Node requirement | ≥ 18 |
