Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Bugsee has a very rich feature set, yet on some platforms we managed to achieve
<tr>
<th>File attachments</th>
<td><a href="/sdk/ios/custom/">View</a></td>
<td><a href="/sdk/android/manual/bug-reporting#attachments">View</a></td>
<td><a href="/sdk/android/report-handler#attachments">View</a></td>
<td><a href="/sdk/cordova/custom/">View</a></td>
<td><a href="/sdk/react_native/custom/#file-attachments">View</a></td>
<td><a href="/sdk/flutter/custom/#file-attachments">View</a></td>
Expand Down
86 changes: 3 additions & 83 deletions docs/sdk/android/manual/bug-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,89 +158,9 @@ Crash, error, and bug reports get a default severity unless one is specified or

## Customize reports with `ReportHandler`

`ReportHandler` is the idiomatic way to inspect or rewrite reports before they are uploaded. It runs for **every** outgoing report — crashes and errors included, not just bug reports.
`ReportHandler` lets you inspect or rewrite a report before it is uploaded, and it runs for **every** outgoing report — crashes and handled errors included, not just the bug reports on this page. It is documented in full on its own page:

Two callbacks are provided as default methods on the interface:

- `onBeforeReportCreated(Report, boolean isTerminating, Runnable completionCallback)` — runs before the report payload is assembled.
- `onAfterReportCreated(Report, boolean isTerminating, Runnable completionCallback)` — runs after assembly, ideal for attachments.

You **must** invoke `completionCallback.run()` for the pipeline to proceed — unless `isTerminating` is `true`, in which case the pipeline continues regardless (the process is about to exit).

<Tabs groupId="lang-android">
<TabItem value="java" label="Java">

```java
Bugsee.setReportHandler(new ReportHandler() {
@Override
public void onBeforeReportCreated(Report report, boolean isTerminating, Runnable completionCallback) {
report.setSummary("[" + BuildConfig.FLAVOR + "] " + report.getSummary());
report.setSeverity(IssueSeverity.High);
completionCallback.run();
}

@Override
public void onAfterReportCreated(Report report, boolean isTerminating, Runnable completionCallback) {
Attachment attachment = report.createAndAddAttachment("config")
.setName("config")
.setFileName("config.json")
.setMimeType("application/json");
try (OutputStream out = attachment.openStream()) {
if (out != null) {
out.write(loadConfigSnapshot());
}
} catch (IOException ignored) {
}
completionCallback.run();
}
});
```

</TabItem>
<TabItem value="kotlin" label="Kotlin">

```kotlin
Bugsee.setReportHandler(object : ReportHandler {
override fun onBeforeReportCreated(report: Report, isTerminating: Boolean, completionCallback: Runnable) {
report.summary = "[${BuildConfig.FLAVOR}] ${report.summary}"
report.severity = IssueSeverity.High
completionCallback.run()
}

override fun onAfterReportCreated(report: Report, isTerminating: Boolean, completionCallback: Runnable) {
report.createAndAddAttachment("config")
.setName("config")
.setFileName("config.json")
.setMimeType("application/json")
.openStream()?.use { it.write(loadConfigSnapshot()) }
completionCallback.run()
}
})
```

</TabItem>
</Tabs>

### Attachments

Add attachments from `onAfterReportCreated`. `Report.createAndAddAttachment(name)` returns a mutable `Attachment` that you populate with fluent setters and write to via its output stream:

- `Attachment setName(String)` — display name shown in the dashboard.
- `Attachment setFileName(String)` — file name (with extension) used for the download.
- `Attachment setMimeType(String)` — MIME type (e.g. `"application/json"`).
- `OutputStream openStream()` — opens the (truncating) stream to write the attachment bytes; may return `null` if the attachment cannot be opened. The caller owns closing the stream and should buffer writes.

The setters return the same `Attachment`, so they can be chained. Reports allow up to **3 attachments × 3 MB**; enforce it inside your own handler if relevant.

### Callback timeout

The SDK waits at most `ReportHandlerCallbackTimeout` seconds (default `30`) for `completionCallback` to fire. Tune via manifest:

```xml
<meta-data
android:name="com.bugsee.option.config.report-handler-callback-timeout"
android:value="10" />
```
- [Report handler](/sdk/android/report-handler) — the two callbacks, the completion contract and its timeout, threading, behaviour during a crash, and attachments.

## Programmatic report creation

Expand All @@ -257,7 +177,7 @@ The `Report` you receive is mutable. The most commonly used members:
- `setSummary(String)` / `setDescription(String)` — the report's title and body text.
- `setSeverity(IssueSeverity)` — one of `VeryLow`, `Medium`, `High`, `Critical`, `Blocker`.
- `addLabel(String)` / `addLabels(List<String>)` / `setLabels(List<String>)` / `clearLabels()` — dashboard labels.
- `getAttachments()` returns the current `List<Attachment>`; `createAndAddAttachment(name)` adds a new one (see [Attachments](#attachments) above).
- `getAttachments()` returns the current `List<Attachment>`; `createAndAddAttachment(name)` adds a new one (see [Attachments](/sdk/android/report-handler#attachments)).

The same `Report` is what `ReportHandler.onBeforeReportCreated(...)` / `onAfterReportCreated(...)` hands you, so the same mutators apply there.

Expand Down
2 changes: 1 addition & 1 deletion docs/sdk/android/manual/crash-error-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@ Bugsee.testCrash();

## Report severity

Crash and error reports receive a default severity — `IssueSeverity.Blocker` for crashes and `IssueSeverity.High` for errors — unless you override it. Change the defaults under [Default severities](/sdk/android/manual/bug-reporting#default-severities), set a severity per call via the `logException(ex, options)` overload, or adjust any report from a [`ReportHandler`](/sdk/android/manual/bug-reporting#customize-reports-with-reporthandler). See [Severity values](/sdk/android/manual/bug-reporting#severity-values) for the full list.
Crash and error reports receive a default severity — `IssueSeverity.Blocker` for crashes and `IssueSeverity.High` for errors — unless you override it. Change the defaults under [Default severities](/sdk/android/manual/bug-reporting#default-severities), set a severity per call via the `logException(ex, options)` overload, or adjust any report from a [`ReportHandler`](/sdk/android/report-handler). See [Severity values](/sdk/android/manual/bug-reporting#severity-values) for the full list.
15 changes: 6 additions & 9 deletions docs/sdk/android/privacy/report.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Bugsee.setReportHandler(new ReportHandler() {
@Override
public void onAfterReportCreated(Report report, boolean isTerminating,
Runnable completionCallback) {
// max 3 attachments × 3 MB — write your data via openStream()
// Write your data through the attachment's output stream
Attachment attachment = report.createAndAddAttachment("config")
.setFileName("config.json")
.setMimeType("application/json");
Expand Down Expand Up @@ -80,7 +80,7 @@ Bugsee.setReportHandler(object : ReportHandler {
isTerminating: Boolean,
completionCallback: Runnable
) {
// max 3 attachments × 3 MB — write your data via openStream()
// Write your data through the attachment's output stream
val attachment = report.createAndAddAttachment("config")
.setFileName("config.json")
.setMimeType("application/json")
Expand All @@ -93,10 +93,7 @@ Bugsee.setReportHandler(object : ReportHandler {
</TabItem>
</Tabs>

The previous quota of **3 attachments × 3 MB each** is preserved — enforce
it inside your own `ReportHandler` if needed.

For programmatic report creation see
[manual reports](/sdk/android/manual/bug-reporting). For the complete list of
listener interfaces see the
[public API reference](/sdk/android/configuration).
The full contract — when each callback runs, what happens during a crash,
which thread you are on, the completion timeout, and how attachments work —
is documented on the [report handler](/sdk/android/report-handler) page. This
page covers only the privacy side of it.
226 changes: 226 additions & 0 deletions docs/sdk/android/report-handler.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
---
title: "Report handler"
description: "Inspect, enrich, or scrub every Bugsee report before it is uploaded with the Android SDK's ReportHandler — callbacks, threading, timeouts, and attachments."
sidebar_position: 5
slug: "/sdk/android/report-handler"
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

A **report handler** is the single place to inspect or rewrite a report before Bugsee uploads it. It runs for **every** outgoing report — crashes, handled errors, user-filed bug reports, and silent uploads alike — so it is the right hook for anything that must apply to all of them: tagging reports with your build variant, raising severity for a subset, attaching a config snapshot, or scrubbing text a user typed into the report form.

Register one handler per process, as early as you can — typically right after `Bugsee.launch(...)`:

<Tabs groupId="lang-android">
<TabItem value="java" label="Java">

```java
Bugsee.setReportHandler(new ReportHandler() {
@Override
public void onBeforeReportCreated(Report report, boolean isTerminating, Runnable completionCallback) {
report.setSummary("[" + BuildConfig.FLAVOR + "] " + report.getSummary());
completionCallback.run();
}
});
```

</TabItem>
<TabItem value="kotlin" label="Kotlin">

```kotlin
Bugsee.setReportHandler(object : ReportHandler {
override fun onBeforeReportCreated(report: Report, isTerminating: Boolean, completionCallback: Runnable) {
report.summary = "[${BuildConfig.FLAVOR}] ${report.summary}"
completionCallback.run()
}
})
```

</TabItem>
</Tabs>

Setting a new handler replaces the previous one. Pass `null` to remove it.

## The two callbacks

Both are `default` methods on the interface, so implement only the one you need.

|Callback|When it runs|Use it for|
|---|---|---|
|`onBeforeReportCreated`|Before the report payload is assembled|Rewriting summary, description, severity, labels, and attributes; scrubbing user-entered text|
|`onAfterReportCreated`|After assembly, before upload|Adding attachments, and any change that should see the assembled report|

Each receives the `Report`, an `isTerminating` flag, and a `completionCallback`.

## Completing the callback

The reporting pipeline **waits** for you: nothing is uploaded until you call `completionCallback.run()`. This is what makes asynchronous work possible — you can hand the report off to a background thread and complete the callback when that work finishes.

Because a handler that never completes would strand the report, the SDK arms a timeout. If `completionCallback` has not fired within `ReportHandlerCallbackTimeout` seconds (default **30**), it fires automatically and the report proceeds with whatever changes you had made by then. Set the option to `0` to disable the timeout.

```xml
<meta-data
android:name="com.bugsee.option.config.report-handler-callback-timeout"
android:value="10" />
```

The callback is safe to invoke more than once — only the first call advances the pipeline.

## Threading

Non-terminating reports invoke your callbacks on the **main thread**. Keep them short: anything slow — file I/O, network calls, compression — should run on your own background thread, with `completionCallback.run()` called when it finishes.

<Tabs groupId="lang-android">
<TabItem value="java" label="Java">

```java
@Override
public void onAfterReportCreated(Report report, boolean isTerminating, Runnable completionCallback) {
if (isTerminating) {
// The process is going away — do the minimum, inline.
completionCallback.run();
return;
}
executor.execute(() -> {
writeDiagnosticsAttachment(report);
completionCallback.run();
});
}
```

</TabItem>
<TabItem value="kotlin" label="Kotlin">

```kotlin
override fun onAfterReportCreated(report: Report, isTerminating: Boolean, completionCallback: Runnable) {
if (isTerminating) {
// The process is going away — do the minimum, inline.
completionCallback.run()
return
}
executor.execute {
writeDiagnosticsAttachment(report)
completionCallback.run()
}
}
```

</TabItem>
</Tabs>

## Crashes and other terminating reports

When `isTerminating` is `true` the process is about to exit, and the rules change:

- Your callback runs on a background thread the SDK already had alive, and the SDK waits only a **few seconds** before finalizing the report anyway.
- Work you schedule for later is lost — the process will not be there to run it. Do the minimum inline and return.
- `completionCallback` no longer gates anything. Call it immediately or not at all; the pipeline continues either way.
- The `ReportHandlerCallbackTimeout` option does not apply to this path.

A handler that behaves well on a crash therefore checks `isTerminating` first, as in the example above.

## If your handler throws

An exception thrown out of either callback is caught and logged, and the pipeline advances so the report is still delivered. Your changes up to the throw are kept. Don't rely on this — it exists so a bug in a handler cannot cost you reports.

## What you can change

The `Report` handed to you exposes:

|Area|Members|
|---|---|
|Identity|`getId()`, `getType()`|
|Text|`getSummary()` / `setSummary(...)`, `getDescription()` / `setDescription(...)`, `getEmail()` / `setEmail(...)`|
|Severity|`getSeverity()` / `setSeverity(IssueSeverity)`|
|Labels|`getLabels()`, `addLabel(...)`, `addLabels(...)`, `setLabels(...)`, `clearLabels()`|
|Attributes|`getAttributes()`, `getAttribute(...)`, `setAttribute(...)`, `removeAttribute(...)`, `clearAllAttributes()`|
|Attachments|`getAttachments()`, `createAndAddAttachment(...)`, `clearAttachments()`|
|Screenshots|`getScreenshot(...)`, `setScreenshot(...)`, `setScreenshotAsync(...)`, `enumerateScreenshots(...)`|

Attributes set here apply to this report only; for values that should ride along with every report, see [user & session data](/sdk/android/user-session-data).

## Attachments

Attachments are added from `onAfterReportCreated`. `createAndAddAttachment(name)` returns a mutable `Attachment` that you describe with fluent setters and fill through its output stream:

- `setName(String)` — display name shown in the dashboard.
- `setFileName(String)` — file name, with extension, used for the download.
- `setMimeType(String)` — MIME type, for example `"application/json"`.
- `openStream()` — opens a truncating `OutputStream` for the attachment's bytes. It may return `null` if the attachment cannot be opened, and you own closing it.

<Tabs groupId="lang-android">
<TabItem value="java" label="Java">

```java
Attachment attachment = report.createAndAddAttachment("config")
.setName("config")
.setFileName("config.json")
.setMimeType("application/json");

try (OutputStream out = attachment.openStream()) {
if (out != null) {
out.write(loadConfigSnapshot());
}
} catch (IOException ignored) {
}
```

</TabItem>
<TabItem value="kotlin" label="Kotlin">

```kotlin
report.createAndAddAttachment("config")
.setName("config")
.setFileName("config.json")
.setMimeType("application/json")
.openStream()?.use { it.write(loadConfigSnapshot()) }
```

</TabItem>
</Tabs>

A single report holds at most **1000** attachments. Past that the SDK logs a warning and skips the attachment — `createAndAddAttachment` still returns an object, but it is not part of the report. Keep attachments small regardless: they travel with the report on the user's connection.

## Example: scrubbing sensitive text

Anything a user types into the report form reaches you before it reaches Bugsee, which makes `onBeforeReportCreated` the place to redact it.

<Tabs groupId="lang-android">
<TabItem value="java" label="Java">

```java
Bugsee.setReportHandler(new ReportHandler() {
@Override
public void onBeforeReportCreated(Report report, boolean isTerminating, Runnable completionCallback) {
String description = report.getDescription();
if (description != null) {
report.setDescription(description.replaceAll("\\d{16}", "[CARD]"));
}
report.removeAttribute("internal_session_token");
completionCallback.run();
}
});
```

</TabItem>
<TabItem value="kotlin" label="Kotlin">

```kotlin
Bugsee.setReportHandler(object : ReportHandler {
override fun onBeforeReportCreated(report: Report, isTerminating: Boolean, completionCallback: Runnable) {
report.description = report.description?.replace(Regex("\\d{16}"), "[CARD]")
report.removeAttribute("internal_session_token")
completionCallback.run()
}
})
```

</TabItem>
</Tabs>

## See also

- [Bug reporting](/sdk/android/manual/bug-reporting) — triggers, the report dialog, and programmatic report creation.
- [Crash & error reporting](/sdk/android/manual/crash-error-reporting) — default severities and handled errors.
- [Privacy and report fields](/sdk/android/privacy/report) — what reaches Bugsee, and the other privacy controls.
1 change: 1 addition & 0 deletions sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const sidebars: SidebarsConfig = {
{ type: "doc", id: "sdk/android/manual/crash-error-reporting", label: "Crash & error reporting" },
],
},
{ type: "doc", id: "sdk/android/report-handler", label: "Report handler" },
{
type: "category",
label: "Data capture",
Expand Down
Loading