Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,77 @@ When integrating BugSplat into your Android application, it's crucial to ensure

These configurations ensure that the BugSplat native libraries are properly included in your app and can function correctly to capture and report native crashes.

## Handled Exceptions 🪤

Not every problem crashes the process. Use `BugSplat.postException` to report a caught `Throwable` as a non-fatal report. Non-fatals appear alongside crashes with the **"Android.Java"** type, so you can filter them separately on the dashboard.

Reports carry the same metadata as a crash: the attributes and attachments passed to `init`, plus anything set with `BugSplat.setAttribute`. `BugSplat.init` must have been called first — a post before init is logged and dropped.

### Reporting a caught exception

**Java**
```java
try {
riskyOperation();
} catch (Exception e) {
// Uploads on a background thread and returns immediately
BugSplat.postException(e);
}
```

**Kotlin**
```kotlin
try {
riskyOperation()
} catch (e: Exception) {
BugSplat.postException(e)
}
```

### Per-report attributes

Attributes passed to `postException` are layered over the attributes already registered with the SDK, so a per-report value wins over one with the same key from `init` or `setAttribute`:

```java
Map<String, String> attributes = new HashMap<>();
attributes.put("screen", "checkout");
attributes.put("retryCount", "2");

BugSplat.postException(e, attributes);
```

### Blocking submission

Use `postExceptionBlocking` when you need the result — for example in a background worker that should not exit until the report is uploaded. It returns `false` if the upload failed, the post was rate limited, or the SDK was not initialized. **Do not call it on the main thread.**

```java
boolean reported = BugSplat.postExceptionBlocking(e);
```

### Rate limiting

A caught exception inside a render or game loop can fire every frame, so the SDK drops posts made within **3000ms** of the previous accepted one. Adjust the window, or disable the guard entirely with a value of zero or less:

```java
BugSplat.setExceptionPostIntervalMillis(10_000); // at most one report per 10s
BugSplat.setExceptionPostIntervalMillis(0); // report every exception
```

### Reporting uncaught exceptions

Crashpad only catches native signals, so a Java exception that reaches the top of the stack is not reported automatically. To capture those, install a default handler that posts the exception before delegating to the previous one:

```java
Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
// The process is about to die, so block until the report is uploaded
BugSplat.postExceptionBlocking(throwable);
if (previous != null) {
previous.uncaughtException(thread, throwable);
}
});
```

## ANR Detection 🐌

The BugSplat Android SDK automatically detects and reports Application Not Responding (ANR) events on Android 11+ (API level 30+) using the [`ApplicationExitInfo`](https://developer.android.com/reference/android/app/ApplicationExitInfo) API.
Expand Down
91 changes: 91 additions & 0 deletions app/src/main/java/com/bugsplat/android/BugSplat.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -109,6 +110,96 @@ public static void removeAttribute(String key) {
BugSplatBridge.removeAttribute(key);
}

/**
* Report a caught {@link Throwable} to BugSplat as a non-fatal error.
* This runs on a background thread and returns immediately.
*
* <p>The report carries the attributes and attachments supplied to
* {@code init}, plus any set with {@link #setAttribute(String, String)}, and
* appears on the dashboard with the {@code Android.Java} crash type so
* non-fatals can be filtered separately from crashes.</p>
*
* <p>Posts closer together than the exception post interval are dropped —
* see {@link #setExceptionPostIntervalMillis(long)}. Requires {@code init}
* to have been called; a post before init is logged and ignored.</p>
*
* @param throwable The caught exception to report
*/
public static void postException(Throwable throwable) {
postException(throwable, null);
}

/**
* Report a caught {@link Throwable} to BugSplat with additional attributes.
* This runs on a background thread and returns immediately.
*
* @param throwable The caught exception to report
* @param attributes Extra key/value attributes for this report, layered over
* the attributes already registered with the SDK, or null
*/
public static void postException(Throwable throwable, Map<String, String> attributes) {
new Thread(() -> postExceptionInternal(throwable, attributes)).start();
}
Comment on lines +140 to +142

/**
* Report a caught {@link Throwable} to BugSplat.
* This blocks until the upload is complete.
*
* @param throwable The caught exception to report
* @return true if the report was uploaded; false if it failed, was rate
* limited, or the SDK was not initialized
*/
public static boolean postExceptionBlocking(Throwable throwable) {
return postExceptionBlocking(throwable, null);
}

/**
* Report a caught {@link Throwable} to BugSplat with additional attributes.
* This blocks until the upload is complete.
*
* @param throwable The caught exception to report
* @param attributes Extra key/value attributes for this report, layered over
* the attributes already registered with the SDK, or null
* @return true if the report was uploaded; false if it failed, was rate
* limited, or the SDK was not initialized
*/
public static boolean postExceptionBlocking(Throwable throwable, Map<String, String> attributes) {
return postExceptionInternal(throwable, attributes);
}

/**
* Set the minimum time between two accepted {@code postException} calls.
*
* <p>A caught exception inside a render or game loop can fire every frame,
* so posts made within this window of the previous one are dropped. Defaults
* to 3000ms. Pass zero or less to disable the guard and post every
* exception.</p>
*
* @param millis The minimum interval in milliseconds
*/
public static void setExceptionPostIntervalMillis(long millis) {
ExceptionReporter.setMinPostIntervalMillis(millis);
}

private static boolean postExceptionInternal(Throwable throwable, Map<String, String> attributes) {
if (throwable == null) {
Log.e("BugSplat", "postException called with a null throwable");
return false;
}
if (!BugSplatConfig.isInitialized()) {
Log.e("BugSplat", "postException called before init; report dropped");
return false;
}
if (!ExceptionReporter.shouldPost(System.currentTimeMillis())) {
Log.w("BugSplat", "postException rate limited; report dropped");
return false;
}

ExceptionReporter reporter = new ExceptionReporter(
BugSplatConfig.database(), BugSplatConfig.application(), BugSplatConfig.version());
return reporter.post(throwable, attributes, BugSplatConfig.attachmentFiles());
}

/**
* Upload debug symbols for native libraries (.so files) in the specified directory.
* This method runs asynchronously and returns immediately.
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/com/bugsplat/android/BugSplatBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public static void initBugSplat(Activity activity, String database, String appli

public static void initBugSplat(Activity activity, String database, String application, String version,
Map<String, String> attributes, String[] attachments) {
// Mirror the init parameters on the Java side — Crashpad owns this state
// natively, and postException needs to read it back to build a report.
BugSplatConfig.init(database, application, version, attributes, attachments);

ApplicationInfo applicationInfo = activity.getApplicationInfo();
Log.d("BugSplat", "init result: " +
jniInitBugSplat(applicationInfo.dataDir, applicationInfo.nativeLibraryDir, database, application,
Expand All @@ -56,14 +60,16 @@ public static void hang() {
public static void setAttribute(String key, String value) {
validateAttributeKey(key);
if (value == null) {
jniRemoveAttribute(key);
removeAttribute(key);
return;
}
BugSplatConfig.setAttribute(key, value);
jniSetAttribute(key, value);
}

public static void removeAttribute(String key) {
validateAttributeKey(key);
BugSplatConfig.removeAttribute(key);
jniRemoveAttribute(key);
}

Expand Down
102 changes: 102 additions & 0 deletions app/src/main/java/com/bugsplat/android/BugSplatConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.bugsplat.android;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Java-side mirror of the values handed to {@code init}.
*
* <p>Crashpad owns this state natively, but native memory is not readable from
* Java, so APIs that build a report in-process — {@link ExceptionReporter} —
* need their own copy of the database/application/version triple plus the
* attributes and attachments that a crash report would carry.</p>
*
* <p>All accessors are safe to call from any thread.</p>
*/
final class BugSplatConfig {
private static volatile String database;
private static volatile String application;
private static volatile String version;
private static final Map<String, String> attributes = new ConcurrentHashMap<>();
private static volatile List<String> attachments = Collections.emptyList();

private BugSplatConfig() {
}

/** Record the init parameters. Replaces any state from a prior init. */
static void init(String database, String application, String version,
Map<String, String> attributes, String[] attachments) {
BugSplatConfig.database = database;
BugSplatConfig.application = application;
BugSplatConfig.version = version;

BugSplatConfig.attributes.clear();
if (attributes != null) {
for (Map.Entry<String, String> entry : attributes.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
BugSplatConfig.attributes.put(entry.getKey(), entry.getValue());
}
}
}

BugSplatConfig.attachments = attachments == null
? Collections.<String>emptyList()
: Collections.unmodifiableList(new ArrayList<>(java.util.Arrays.asList(attachments)));
}

/** True once {@code init} has supplied the database/application/version triple. */
static boolean isInitialized() {
return database != null && application != null && version != null;
}

static String database() {
return database;
}

static String application() {
return application;
}

static String version() {
return version;
}

static void setAttribute(String key, String value) {
attributes.put(key, value);
}

static void removeAttribute(String key) {
attributes.remove(key);
}

/** A point-in-time copy of the attributes, in no particular order. */
static Map<String, String> attributesSnapshot() {
return new LinkedHashMap<>(attributes);
}

/** The init attachments as {@link File}s; missing paths are left for the zip builder to skip. */
static List<File> attachmentFiles() {
List<String> paths = attachments;
List<File> files = new ArrayList<>(paths.size());
for (String path : paths) {
if (path != null && !path.isEmpty()) {
files.add(new File(path));
}
}
return files;
}

/** Test hook — clears all recorded state. */
static void reset() {
database = null;
application = null;
version = null;
attributes.clear();
attachments = Collections.emptyList();
}
}
Loading
Loading