Skip to content

Latest commit

 

History

History

README.md

Bugstr

🚧 Proceed with caution: Bugstr Android is proof of concept stage, and has not been reviewed by a professional developer 🚧

Bugstr packages the crash reporting flow that Amethyst uses to prompt users to share stack traces with developers over expiring (NIP-17) direct messages. Bugstr includes Quartz, or Android SDK via Amethyst. It is designed to be re-used by other Nostr apps—or any Android app that wants an opt-in crash reporter that keeps the user in control of what is sent.

image

Supported Platforms

Components

Bugstr ships four building blocks:

  1. BugstrCrashReportCache stores crash stack traces on disk. It defaults to one slot; set maxReports or a custom slotKey for multi-slot rotation. All disk I/O is suspend and runs on Dispatchers.IO.
  2. BugstrCrashHandler installs an UncaughtExceptionHandler, accepts an attachments provider, and blocks the crashing thread with a bounded timeout while flushing to disk.
  3. BugstrCrashPrompt is a Jetpack Compose dialog that surfaces all cached reports (newest first) and lets the user send, keep, or dismiss them.
  4. BugstrAnrWatcher (optional) can write a synthetic report when the main thread stalls.

Installing the crash handler

class MyApp : Application() {
    private val bugstrCache by lazy { BugstrCrashReportCache(this, maxReports = 3) }
    private val bugstrHandler by lazy {
        BugstrCrashHandler(
            cache = bugstrCache,
            assembler = BugstrReportAssembler(
                appName = "My App",
                appVersionName = BuildConfig.VERSION_NAME,
                buildVariant = BuildConfig.FLAVOR.ifBlank { "release" },
            ),
            attachmentsProvider = { mapOf("recent logs" to fetchRecentLogs()) },
            writeTimeoutMs = 1_000,
        )
    }

    override fun onCreate() {
        super.onCreate()
        bugstrHandler.installAsDefault()
    }
}

This mirrors the way Amethyst keeps the default handler and only writes non-OOM crashes to disk.

Optional ANR watcher

If you also want ANR coverage, wire up BugstrAnrWatcher:

private val anrWatcher by lazy {
    BugstrAnrWatcher(
        cache = bugstrCache,
        assembler = BugstrReportAssembler(
            appName = "My App",
            appVersionName = BuildConfig.VERSION_NAME,
            buildVariant = BuildConfig.FLAVOR.ifBlank { "release" },
        )
    )
}

override fun onCreate() {
    super.onCreate()
    bugstrHandler.installAsDefault()
    anrWatcher.start()
}

Showing the prompt

In any Compose screen you can drop BugstrCrashPrompt and wire up the onSendReport callback to your own navigation or DM composer. Bugstr will load and delete the cached crash reports on the first composition.

@Composable
fun CrashReportEntryPoint(
    accountViewModel: AccountViewModel,
    nav: INav,
) {
    BugstrCrashPrompt(
        cache = Amethyst.instance.crashReportCache,
        developerName = "Amethyst",
        onSendReport = { stack ->
            nav.nav {
                routeToMessage(
                    user = LocalCache.getOrCreateUser(AMETHYST_DEV_PUBKEY),
                    draftMessage = stack,
                    accountViewModel = accountViewModel,
                    expiresDays = 30, // <- enables expiring NIP-17 DMs
                )
            }
        },
    )
}

In this example the expiresDays flag is what turns the DM composer into a NIP-17 ephemeral message, so the crash report vanishes for everyone after 30 days.

Customizing strings

BugstrCrashPrompt exposes optional parameters (titleText, descriptionText, sendButtonText, dismissButtonText, retryButtonText, loadingText) so you can plug in your own localized strings or keep Bugstr’s defaults. The default copy now reminds users that stack traces might contain personal data.

Notes

  • Bugstr avoids reading or sending anything automatically. Users stay in control and can inspect/edit the crash report before sharing.
  • You can store multiple crashes by setting maxReports or providing a slot key when writing. The prompt iterates through everything it finds.
  • BugstrCrashPrompt offers a “Keep for later” button that rewrites the remaining reports to disk instead of discarding them.
  • BugstrReportAssembler recurses through the entire Throwable cause chain, trims overly large traces (default 200k characters), and intentionally omits Build.HOST/Build.USER to keep ROM build metadata out of the report. Tune maxStackCharacters if needed.
  • Attachments are supported via the crash handler's attachmentsProvider. They render under their own headings and are truncated for safety.

NIP-17 Crypto Module

The bugstr-nostr-crypto module provides standalone NIP-17/44/59 gift wrap building:

val builder = Nip17PayloadBuilder(giftWrapper)
val wraps = builder.buildGiftWraps(
    Nip17Request(
        senderPubKey = myPubKey,
        senderPrivateKeyHex = myPrivKey,
        recipients = listOf(Nip17Recipient(pubKeyHex = devPubKey)),
        plaintext = crashReport,
        expirationSeconds = 30 * 24 * 60 * 60, // 30 days
    )
)
// Sign and publish wraps.map { it.giftWrap } to relays

NIP Compliance

The implementation follows:

  • NIP-17 - Private Direct Messages (kind 14 rumors)
  • NIP-44 - Versioned Encryption (v2)
  • NIP-59 - Gift Wrap (rumor → seal → gift wrap)
  • NIP-40 - Expiration Timestamp

Important: Rumors include id (computed) and sig: "" (empty string) per spec. Some clients reject messages without these fields.

Contributing

See AGENTS.md for contributor guidelines.